diff --git a/rinja_derive/src/generator.rs b/rinja_derive/src/generator.rs index 3318b45a9..372de0cda 100644 --- a/rinja_derive/src/generator.rs +++ b/rinja_derive/src/generator.rs @@ -1,4 +1,5 @@ mod expr; +mod filters; mod node; use std::borrow::Cow; diff --git a/rinja_derive/src/generator/expr.rs b/rinja_derive/src/generator/expr.rs index cd03bbcf9..68305f07a 100644 --- a/rinja_derive/src/generator/expr.rs +++ b/rinja_derive/src/generator/expr.rs @@ -1,18 +1,15 @@ use std::borrow::Cow; use parser::node::CondTest; -use parser::{ - Attr, CharLit, CharPrefix, Expr, Filter, IntKind, Num, Span, StrLit, StrPrefix, Target, - TyGenerics, WithSpan, -}; +use parser::{Attr, CharLit, CharPrefix, Expr, Filter, Span, StrLit, Target, TyGenerics, WithSpan}; use super::{ - DisplayWrap, FILTER_SOURCE, Generator, LocalMeta, TargetIsize, TargetUsize, Writable, - compile_time_escape, is_copyable, normalize_identifier, + DisplayWrap, FILTER_SOURCE, Generator, LocalMeta, Writable, compile_time_escape, is_copyable, + normalize_identifier, }; +use crate::CompileError; use crate::heritage::Context; use crate::integration::Buffer; -use crate::{BUILTIN_FILTERS, BUILTIN_FILTERS_NEED_ALLOC, CompileError, MsgValidEscapers}; impl<'a> Generator<'a, '_> { pub(crate) fn visit_expr_root( @@ -239,445 +236,7 @@ impl<'a> Generator<'a, '_> { DisplayWrap::Unwrapped } - pub(crate) fn visit_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - name: &str, - args: &[WithSpan<'_, Expr<'a>>], - generics: &[WithSpan<'_, TyGenerics<'_>>], - node: Span<'_>, - ) -> Result { - let filter = match name { - "deref" => Self::_visit_deref_filter, - "escape" | "e" => Self::_visit_escape_filter, - "filesizeformat" => Self::_visit_humansize, - "fmt" => Self::_visit_fmt_filter, - "format" => Self::_visit_format_filter, - "join" => Self::_visit_join_filter, - "json" | "tojson" => Self::_visit_json_filter, - "linebreaks" => Self::_visit_linebreaks_filter, - "linebreaksbr" => Self::_visit_linebreaksbr_filter, - "paragraphbreaks" => Self::_visit_paragraphbreaks_filter, - "pluralize" => Self::_visit_pluralize_filter, - "ref" => Self::_visit_ref_filter, - "safe" => Self::_visit_safe_filter, - "urlencode" => Self::_visit_urlencode_filter, - "urlencode_strict" => Self::_visit_urlencode_strict_filter, - "value" => return self._visit_value(ctx, buf, args, generics, node, "`value` filter"), - name if BUILTIN_FILTERS.contains(&name) => { - return self._visit_builtin_filter(ctx, buf, name, args, generics, node); - } - _ => return self._visit_custom_filter(ctx, buf, name, args, generics, node), - }; - if !generics.is_empty() { - Err(ctx.generate_error(format_args!("unexpected generics on filter `{name}`"), node)) - } else { - filter(self, ctx, buf, args, node) - } - } - - fn _visit_custom_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - name: &str, - args: &[WithSpan<'_, Expr<'a>>], - generics: &[WithSpan<'_, TyGenerics<'_>>], - node: Span<'_>, - ) -> Result { - if BUILTIN_FILTERS_NEED_ALLOC.contains(&name) { - ensure_filter_has_feature_alloc(ctx, name, node)?; - } - buf.write(format_args!("filters::{name}")); - self.visit_call_generics(buf, generics); - buf.write('('); - self._visit_args(ctx, buf, args)?; - buf.write(")?"); - Ok(DisplayWrap::Unwrapped) - } - - fn _visit_builtin_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - name: &str, - args: &[WithSpan<'_, Expr<'a>>], - generics: &[WithSpan<'_, TyGenerics<'_>>], - node: Span<'_>, - ) -> Result { - if !generics.is_empty() { - return Err( - ctx.generate_error(format_args!("unexpected generics on filter `{name}`"), node) - ); - } - buf.write(format_args!("rinja::filters::{name}")); - self.visit_call_generics(buf, generics); - buf.write('('); - self._visit_args(ctx, buf, args)?; - buf.write(")?"); - Ok(DisplayWrap::Unwrapped) - } - - fn _visit_urlencode_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - self._visit_urlencode_filter_inner(ctx, buf, "urlencode", args, node) - } - - fn _visit_urlencode_strict_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - self._visit_urlencode_filter_inner(ctx, buf, "urlencode_strict", args, node) - } - - fn _visit_urlencode_filter_inner( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - name: &str, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - if cfg!(not(feature = "urlencode")) { - return Err(ctx.generate_error( - format_args!("the `{name}` filter requires the `urlencode` feature to be enabled"), - node, - )); - } - - // Both filters return HTML-safe strings. - buf.write(format_args!( - "rinja::filters::HtmlSafeOutput(rinja::filters::{name}(", - )); - self._visit_args(ctx, buf, args)?; - buf.write(")?)"); - Ok(DisplayWrap::Unwrapped) - } - - fn _visit_humansize( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - _node: Span<'_>, - ) -> Result { - // All filters return numbers, and any default formatted number is HTML safe. - buf.write(format_args!( - "rinja::filters::HtmlSafeOutput(rinja::filters::filesizeformat(\ - rinja::helpers::get_primitive_value(&(" - )); - self._visit_args(ctx, buf, args)?; - buf.write(")) as rinja::helpers::core::primitive::f32)?)"); - Ok(DisplayWrap::Unwrapped) - } - - fn _visit_pluralize_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - const SINGULAR: &WithSpan<'static, Expr<'static>> = - &WithSpan::new_without_span(Expr::StrLit(StrLit { - prefix: None, - content: "", - })); - const PLURAL: &WithSpan<'static, Expr<'static>> = - &WithSpan::new_without_span(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("rinja::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(")?"); - } - Ok(DisplayWrap::Wrapped) - } - - fn _visit_paragraphbreaks_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - self._visit_linebreaks_filters(ctx, buf, "paragraphbreaks", args, node) - } - - fn _visit_linebreaksbr_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - self._visit_linebreaks_filters(ctx, buf, "linebreaksbr", args, node) - } - - fn _visit_linebreaks_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - self._visit_linebreaks_filters(ctx, buf, "linebreaks", args, node) - } - - fn _visit_linebreaks_filters( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - name: &str, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - ensure_filter_has_feature_alloc(ctx, name, node)?; - if args.len() != 1 { - return Err(ctx.generate_error( - format_args!("unexpected argument(s) in `{name}` filter"), - node, - )); - } - buf.write(format_args!( - "rinja::filters::{name}(&(&&rinja::filters::AutoEscaper::new(&(", - )); - self._visit_args(ctx, buf, args)?; - // The input is always HTML escaped, regardless of the selected escaper: - buf.write("), rinja::filters::Html)).rinja_auto_escape()?)?"); - // The output is marked as HTML safe, not safe in all contexts: - Ok(DisplayWrap::Unwrapped) - } - - fn _visit_ref_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - let arg = match args { - [arg] => arg, - _ => return Err(ctx.generate_error("unexpected argument(s) in `as_ref` filter", node)), - }; - buf.write('&'); - self.visit_expr(ctx, buf, arg)?; - Ok(DisplayWrap::Unwrapped) - } - - fn _visit_deref_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - let arg = match args { - [arg] => arg, - _ => return Err(ctx.generate_error("unexpected argument(s) in `deref` filter", node)), - }; - buf.write('*'); - self.visit_expr(ctx, buf, arg)?; - Ok(DisplayWrap::Unwrapped) - } - - fn _visit_json_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - if cfg!(not(feature = "serde_json")) { - return Err(ctx.generate_error( - "the `json` filter requires the `serde_json` feature to be enabled", - node, - )); - } - - let filter = match args.len() { - 1 => "json", - 2 => "json_pretty", - _ => return Err(ctx.generate_error("unexpected argument(s) in `json` filter", node)), - }; - buf.write(format_args!("rinja::filters::{filter}(")); - self._visit_args(ctx, buf, args)?; - buf.write(")?"); - Ok(DisplayWrap::Unwrapped) - } - - fn _visit_safe_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - if args.len() != 1 { - return Err(ctx.generate_error("unexpected argument(s) in `safe` filter", node)); - } - buf.write("rinja::filters::safe("); - self._visit_args(ctx, buf, args)?; - buf.write(format_args!(", {})?", self.input.escaper)); - Ok(DisplayWrap::Wrapped) - } - - fn _visit_escape_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - if args.len() > 2 { - return Err(ctx.generate_error("only two arguments allowed to escape filter", node)); - } - let opt_escaper = match args.get(1).map(|expr| &**expr) { - Some(Expr::StrLit(StrLit { prefix, content })) => { - if let Some(prefix) = prefix { - let kind = if *prefix == StrPrefix::Binary { - "slice" - } else { - "CStr" - }; - return Err(ctx.generate_error( - format_args!( - "invalid escaper `b{content:?}`. Expected a string, found a {kind}" - ), - args[1].span(), - )); - } - Some(content) - } - Some(_) => { - return Err(ctx.generate_error("invalid escaper type for escape filter", node)); - } - None => None, - }; - let escaper = match opt_escaper { - Some(name) => self - .input - .config - .escapers - .iter() - .find_map(|(extensions, path)| { - extensions - .contains(&Cow::Borrowed(name)) - .then_some(path.as_ref()) - }) - .ok_or_else(|| { - ctx.generate_error( - format_args!( - "invalid escaper '{name}' for `escape` filter. {}", - MsgValidEscapers(&self.input.config.escapers), - ), - node, - ) - })?, - None => self.input.escaper, - }; - buf.write("rinja::filters::escape("); - self._visit_args(ctx, buf, &args[..1])?; - buf.write(format_args!(", {escaper})?")); - Ok(DisplayWrap::Wrapped) - } - - fn _visit_format_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - ensure_filter_has_feature_alloc(ctx, "format", node)?; - if !args.is_empty() { - if let Expr::StrLit(ref fmt) = *args[0] { - buf.write("rinja::helpers::alloc::format!("); - self.visit_str_lit(buf, fmt); - if args.len() > 1 { - buf.write(','); - self._visit_args(ctx, buf, &args[1..])?; - } - buf.write(')'); - return Ok(DisplayWrap::Unwrapped); - } - } - Err(ctx.generate_error(r#"use filter format like `"a={} b={}"|format(a, b)`"#, node)) - } - - fn _visit_fmt_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - ensure_filter_has_feature_alloc(ctx, "fmt", node)?; - if let [_, arg2] = args { - if let Expr::StrLit(ref fmt) = **arg2 { - buf.write("rinja::helpers::alloc::format!("); - self.visit_str_lit(buf, fmt); - buf.write(','); - self._visit_args(ctx, buf, &args[..1])?; - buf.write(')'); - return Ok(DisplayWrap::Unwrapped); - } - } - Err(ctx.generate_error(r#"use filter fmt like `value|fmt("{:?}")`"#, node)) - } - - // Force type coercion on first argument to `join` filter (see #39). - fn _visit_join_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - args: &[WithSpan<'_, Expr<'a>>], - _node: Span<'_>, - ) -> Result { - buf.write("rinja::filters::join((&"); - for (i, arg) in args.iter().enumerate() { - if i > 0 { - buf.write(", &"); - } - self.visit_expr(ctx, buf, arg)?; - if i == 0 { - buf.write(").into_iter()"); - } - } - buf.write(")?"); - Ok(DisplayWrap::Unwrapped) - } - - fn _visit_value( + pub(super) fn _visit_value( &mut self, ctx: &Context<'_>, buf: &mut Buffer, @@ -708,7 +267,7 @@ impl<'a> Generator<'a, '_> { Ok(DisplayWrap::Unwrapped) } - fn _visit_args( + pub(super) fn _visit_args( &mut self, ctx: &Context<'_>, buf: &mut Buffer, @@ -723,7 +282,7 @@ impl<'a> Generator<'a, '_> { Ok(()) } - fn _visit_arg( + pub(super) fn _visit_arg( &mut self, ctx: &Context<'_>, buf: &mut Buffer, @@ -765,7 +324,7 @@ impl<'a> Generator<'a, '_> { Ok(()) } - fn _visit_auto_escaped_arg( + pub(crate) fn _visit_auto_escaped_arg( &mut self, ctx: &Context<'_>, buf: &mut Buffer, @@ -822,7 +381,11 @@ impl<'a> Generator<'a, '_> { Ok(DisplayWrap::Unwrapped) } - fn visit_call_generics(&mut self, buf: &mut Buffer, generics: &[WithSpan<'_, TyGenerics<'_>>]) { + pub(super) fn visit_call_generics( + &mut self, + buf: &mut Buffer, + generics: &[WithSpan<'_, TyGenerics<'_>>], + ) { if generics.is_empty() { return; } @@ -1092,7 +655,7 @@ impl<'a> Generator<'a, '_> { DisplayWrap::Unwrapped } - fn visit_str_lit(&mut self, buf: &mut Buffer, s: &StrLit<'_>) -> DisplayWrap { + pub(super) fn visit_str_lit(&mut self, buf: &mut Buffer, s: &StrLit<'_>) -> DisplayWrap { if let Some(prefix) = s.prefix { buf.write(prefix.to_char()); } @@ -1216,76 +779,3 @@ impl<'a> Generator<'a, '_> { } } } - -fn ensure_filter_has_feature_alloc( - ctx: &Context<'_>, - name: &str, - node: Span<'_>, -) -> Result<(), CompileError> { - if !cfg!(feature = "alloc") { - return Err(ctx.generate_error( - format_args!("the `{name}` filter requires the `alloc` feature to be enabled"), - node, - )); - } - Ok(()) -} - -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) - } - - macro_rules! impl_match { - ( - $kind:ident $value:ident; - $($svar:ident => $sty:ident),*; - $($uvar:ident => $uty:ident),*; - ) => { - match $kind { - $( - Some(IntKind::$svar) => is_signed_singular($sty::from_str_radix, $value, 1, -1), - )* - $( - Some(IntKind::$uvar) => is_unsigned_singular($uty::from_str_radix, $value, 1), - )* - 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), - }, - } - }; - } - - let Expr::NumLit(_, Num::Int(value, kind)) = **expr else { - return None; - }; - impl_match! { - kind value; - I8 => i8, - I16 => i16, - I32 => i32, - I64 => i64, - I128 => i128, - Isize => TargetIsize; - U8 => u8, - U16 => u16, - U32 => u32, - U64 => u64, - U128 => u128, - Usize => TargetUsize; - } -} diff --git a/rinja_derive/src/generator/filters.rs b/rinja_derive/src/generator/filters.rs new file mode 100644 index 000000000..c3609bdf6 --- /dev/null +++ b/rinja_derive/src/generator/filters.rs @@ -0,0 +1,563 @@ +use std::borrow::Cow; +use std::fmt; + +use parser::{Expr, IntKind, Num, Span, StrLit, StrPrefix, TyGenerics, WithSpan}; + +use super::{Buffer, Context, DisplayWrap, Generator}; +use crate::generator::{TargetIsize, TargetUsize}; +use crate::{CompileError, MsgValidEscapers}; + +impl<'a> Generator<'a, '_> { + pub(crate) fn visit_filter( + &mut self, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + generics: &[WithSpan<'_, TyGenerics<'_>>], + node: Span<'_>, + ) -> Result { + /// Wraps and shadows `$fn` with a function that checks that no generics were supplied, + /// then calls the original `$fn` without `generics`. + macro_rules! filters_without_generics { + ($($fn:ident),+ $(,)?) => { $( + fn $fn<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + generics: &[WithSpan<'_, TyGenerics<'_>>], + node: Span<'_>, + ) -> Result { + ensure_filter_has_no_generics(ctx, name, generics, node)?; + self::$fn(this, ctx, buf, name, args, node) + } + )+ }; + } + + filters_without_generics! { + deref, + escape, + humansize, + fmt, + format, + join, + json, + linebreaks, + pluralize, + r#ref, + safe, + urlencode, + builtin, + } + + let filter = match name { + "capitalize" | "center" | "indent" | "lower" | "lowercase" | "title" | "trim" + | "truncate" | "upper" | "uppercase" | "wordcount" => builtin, + "deref" => deref, + "e" | "escape" => escape, + "filesizeformat" => humansize, + "fmt" => fmt, + "format" => format, + "join" => join, + "json" | "tojson" => json, + "linebreaks" | "linebreaksbr" | "paragraphbreaks" => linebreaks, + "pluralize" => pluralize, + "ref" => r#ref, + "safe" => safe, + "urlencode" | "urlencode_strict" => urlencode, + "value" => value, + _ => custom, + }; + filter(self, ctx, buf, name, args, generics, node) + } +} + +fn custom<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + generics: &[WithSpan<'_, TyGenerics<'_>>], + _node: Span<'_>, +) -> Result { + buf.write(format_args!("filters::{name}")); + this.visit_call_generics(buf, generics); + buf.write('('); + this._visit_args(ctx, buf, args)?; + buf.write(")?"); + Ok(DisplayWrap::Unwrapped) +} + +fn value<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + _name: &str, + args: &[WithSpan<'_, Expr<'a>>], + generics: &[WithSpan<'_, TyGenerics<'_>>], + node: Span<'_>, +) -> Result { + this._visit_value(ctx, buf, args, generics, node, "`value` filter") +} + +fn builtin<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + node: Span<'_>, +) -> Result { + if matches!(name, "center" | "truncate") { + ensure_filter_has_feature_alloc(ctx, name, node)?; + } + buf.write(format_args!("rinja::filters::{name}(")); + this._visit_args(ctx, buf, args)?; + buf.write(")?"); + Ok(DisplayWrap::Unwrapped) +} + +fn urlencode<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + node: Span<'_>, +) -> Result { + if cfg!(not(feature = "urlencode")) { + return Err(ctx.generate_error( + format_args!("the `{name}` filter requires the `urlencode` feature to be enabled"), + node, + )); + } + + let arg = get_filter_argument(ctx, name, args, node)?; + // Both filters return HTML-safe strings. + buf.write(format_args!( + "rinja::filters::HtmlSafeOutput(rinja::filters::{name}(", + )); + this._visit_arg(ctx, buf, arg)?; + buf.write(")?)"); + Ok(DisplayWrap::Unwrapped) +} + +fn humansize<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + node: Span<'_>, +) -> Result { + let arg = get_filter_argument(ctx, name, args, node)?; + // All filters return numbers, and any default formatted number is HTML safe. + buf.write(format_args!( + "rinja::filters::HtmlSafeOutput(rinja::filters::filesizeformat(\ + rinja::helpers::get_primitive_value(&(" + )); + this._visit_arg(ctx, buf, arg)?; + buf.write(")) as rinja::helpers::core::primitive::f32)?)"); + Ok(DisplayWrap::Unwrapped) +} + +fn pluralize<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + node: Span<'_>, +) -> Result { + const SINGULAR: &WithSpan<'static, Expr<'static>> = + &WithSpan::new_without_span(Expr::StrLit(StrLit { + prefix: None, + content: "", + })); + const PLURAL: &WithSpan<'static, Expr<'static>> = + &WithSpan::new_without_span(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(unexpected_filter_arguments(ctx, name, args, node, 2)), + }; + if let Some(is_singular) = expr_is_int_lit_plus_minus_one(count) { + let value = if is_singular { sg } else { pl }; + this._visit_auto_escaped_arg(ctx, buf, value)?; + } else { + buf.write("rinja::filters::pluralize("); + this._visit_arg(ctx, buf, count)?; + for value in [sg, pl] { + buf.write(','); + this._visit_auto_escaped_arg(ctx, buf, value)?; + } + buf.write(")?"); + } + Ok(DisplayWrap::Wrapped) +} + +fn linebreaks<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + node: Span<'_>, +) -> Result { + ensure_filter_has_feature_alloc(ctx, name, node)?; + let arg = get_filter_argument(ctx, name, args, node)?; + buf.write(format_args!( + "rinja::filters::{name}(&(&&rinja::filters::AutoEscaper::new(&(", + )); + this._visit_arg(ctx, buf, arg)?; + // The input is always HTML escaped, regardless of the selected escaper: + buf.write("), rinja::filters::Html)).rinja_auto_escape()?)?"); + // The output is marked as HTML safe, not safe in all contexts: + Ok(DisplayWrap::Unwrapped) +} + +fn r#ref<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + node: Span<'_>, +) -> Result { + let arg = get_filter_argument(ctx, name, args, node)?; + buf.write('&'); + this.visit_expr(ctx, buf, arg)?; + Ok(DisplayWrap::Unwrapped) +} + +fn deref<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + node: Span<'_>, +) -> Result { + let arg = get_filter_argument(ctx, name, args, node)?; + buf.write('*'); + this.visit_expr(ctx, buf, arg)?; + Ok(DisplayWrap::Unwrapped) +} + +fn json<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + node: Span<'_>, +) -> Result { + if cfg!(not(feature = "serde_json")) { + return Err(ctx.generate_error( + "the `json` filter requires the `serde_json` feature to be enabled", + node, + )); + } + + let filter = match args.len() { + 1 => "json", + 2 => "json_pretty", + _ => return Err(unexpected_filter_arguments(ctx, name, args, node, 1)), + }; + buf.write(format_args!("rinja::filters::{filter}(")); + this._visit_args(ctx, buf, args)?; + buf.write(")?"); + Ok(DisplayWrap::Unwrapped) +} + +fn safe<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + node: Span<'_>, +) -> Result { + let arg = get_filter_argument(ctx, name, args, node)?; + buf.write("rinja::filters::safe("); + this._visit_arg(ctx, buf, arg)?; + buf.write(format_args!(", {})?", this.input.escaper)); + Ok(DisplayWrap::Wrapped) +} + +fn escape<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + node: Span<'_>, +) -> Result { + let (arg, escaper) = match args { + [arg] => (arg, this.input.escaper), + [arg, escaper] => { + let Expr::StrLit(StrLit { + ref prefix, + content, + }) = **escaper + else { + return Err(ctx.generate_error( + format_args!("expected string literal for `{name}` filter"), + node, + )); + }; + if let Some(prefix) = prefix { + let kind = match prefix { + StrPrefix::Binary => "slice", + StrPrefix::CLike => "CStr", + }; + return Err(ctx.generate_error( + format_args!("expected string literal for `{name}` filter, got a {kind}"), + args[1].span(), + )); + } + let escaper = this + .input + .config + .escapers + .iter() + .find_map(|(extensions, path)| { + extensions + .contains(&Cow::Borrowed(content)) + .then_some(path.as_ref()) + }) + .ok_or_else(|| { + ctx.generate_error( + format_args!( + "invalid escaper '{content}' for `{name}` filter. {}", + MsgValidEscapers(&this.input.config.escapers), + ), + node, + ) + })?; + (arg, escaper) + } + args => return Err(unexpected_filter_arguments(ctx, name, args, node, 1)), + }; + buf.write("rinja::filters::escape("); + this._visit_arg(ctx, buf, arg)?; + buf.write(format_args!(", {escaper})?")); + Ok(DisplayWrap::Wrapped) +} + +fn format<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + node: Span<'_>, +) -> Result { + ensure_filter_has_feature_alloc(ctx, name, node)?; + if let [fmt, args @ ..] = args { + if let Expr::StrLit(fmt) = &**fmt { + buf.write("rinja::helpers::alloc::format!("); + this.visit_str_lit(buf, fmt); + if !args.is_empty() { + buf.write(','); + this._visit_args(ctx, buf, args)?; + } + buf.write(')'); + return Ok(DisplayWrap::Unwrapped); + } + } + Err(ctx.generate_error(r#"use filter format like `"a={} b={}"|format(a, b)`"#, node)) +} + +fn fmt<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + node: Span<'_>, +) -> Result { + ensure_filter_has_feature_alloc(ctx, name, node)?; + if let [arg, fmt] = args { + if let Expr::StrLit(fmt) = &**fmt { + buf.write("rinja::helpers::alloc::format!("); + this.visit_str_lit(buf, fmt); + buf.write(','); + this._visit_arg(ctx, buf, arg)?; + buf.write(')'); + return Ok(DisplayWrap::Unwrapped); + } + } + Err(ctx.generate_error(r#"use filter fmt like `value|fmt("{:?}")`"#, node)) +} + +fn join<'a>( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + _name: &str, + args: &[WithSpan<'_, Expr<'a>>], + _node: Span<'_>, +) -> Result { + buf.write("rinja::filters::join((&"); + for (i, arg) in args.iter().enumerate() { + if i > 0 { + buf.write(", &"); + } + this.visit_expr(ctx, buf, arg)?; + if i == 0 { + buf.write(").into_iter()"); + } + } + buf.write(")?"); + Ok(DisplayWrap::Unwrapped) +} + +fn ensure_filter_has_feature_alloc( + ctx: &Context<'_>, + name: &str, + node: Span<'_>, +) -> Result<(), CompileError> { + if !cfg!(feature = "alloc") { + return Err(ctx.generate_error( + format_args!("the `{name}` filter requires the `alloc` feature to be enabled"), + node, + )); + } + Ok(()) +} + +#[inline] +fn ensure_filter_has_no_generics( + ctx: &Context<'_>, + name: &str, + generics: &[WithSpan<'_, TyGenerics<'_>>], + node: Span<'_>, +) -> Result<(), CompileError> { + match generics { + [] => Ok(()), + _ => Err(unexpected_filter_generics(ctx, name, node)), + } +} + +#[cold] +fn unexpected_filter_generics(ctx: &Context<'_>, name: &str, node: Span<'_>) -> CompileError { + ctx.generate_error(format_args!("unexpected generics on filter `{name}`"), node) +} + +#[inline] +fn get_filter_argument<'a, 'b>( + ctx: &Context<'_>, + name: &str, + args: &'b [WithSpan<'b, Expr<'a>>], + node: Span<'_>, +) -> Result<&'b WithSpan<'b, Expr<'a>>, CompileError> { + match args { + [arg] => Ok(arg), + _ => Err(unexpected_filter_arguments(ctx, name, args, node, 0)), + } +} + +#[cold] +fn unexpected_filter_arguments( + ctx: &Context<'_>, + name: &str, + args: &[WithSpan<'_, Expr<'_>>], + node: Span<'_>, + at_most: usize, +) -> CompileError { + #[derive(Debug, Clone, Copy)] + struct Error<'a> { + name: &'a str, + count: usize, + at_most: usize, + } + + impl fmt::Display for Error<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "filter `{}` expects ", self.name)?; + match self.at_most { + 0 => f.write_str("no arguments"), + 1 => f.write_str("at most one optional argument"), + n => write!(f, "at most {n} optional arguments"), + }?; + write!(f, ", got {}", self.count - 1) + } + } + + ctx.generate_error( + Error { + name, + count: args.len(), + at_most, + }, + node, + ) +} + +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) + } + + macro_rules! impl_match { + ( + $kind:ident $value:ident; + $($svar:ident => $sty:ident),*; + $($uvar:ident => $uty:ident),*; + ) => { + match $kind { + $( + Some(IntKind::$svar) => is_signed_singular($sty::from_str_radix, $value, 1, -1), + )* + $( + Some(IntKind::$uvar) => is_unsigned_singular($uty::from_str_radix, $value, 1), + )* + 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), + }, + } + }; + } + + let Expr::NumLit(_, Num::Int(value, kind)) = **expr else { + return None; + }; + impl_match! { + kind value; + I8 => i8, + I16 => i16, + I32 => i32, + I64 => i64, + I128 => i128, + Isize => TargetIsize; + U8 => u8, + U16 => u16, + U32 => u32, + U64 => u64, + U128 => u128, + Usize => TargetUsize; + } +} diff --git a/rinja_derive/src/lib.rs b/rinja_derive/src/lib.rs index 9ed99bd9e..68e950329 100644 --- a/rinja_derive/src/lib.rs +++ b/rinja_derive/src/lib.rs @@ -465,22 +465,3 @@ macro_rules! fmt_right { } pub(crate) use {fmt_left, fmt_right}; - -// This is used by the code generator to decide whether a named filter is part of -// Rinja or should refer to a local `filters` module. -const BUILTIN_FILTERS: &[&str] = &[ - "capitalize", - "center", - "indent", - "lower", - "lowercase", - "title", - "trim", - "truncate", - "upper", - "uppercase", - "wordcount", -]; - -// Built-in filters that need the `alloc` feature. -const BUILTIN_FILTERS_NEED_ALLOC: &[&str] = &["center", "truncate"]; diff --git a/rinja_derive_standalone/Cargo.toml b/rinja_derive_standalone/Cargo.toml index 636863837..9786fcec6 100644 --- a/rinja_derive_standalone/Cargo.toml +++ b/rinja_derive_standalone/Cargo.toml @@ -20,6 +20,11 @@ name = "derive-template" harness = false required-features = ["__standalone"] +[[bench]] +name = "filters" +harness = false +required-features = ["__standalone"] + [dependencies] parser = { package = "rinja_parser", version = "=0.3.5", path = "../rinja_parser" } diff --git a/rinja_derive_standalone/benches/filters.rs b/rinja_derive_standalone/benches/filters.rs new file mode 100644 index 000000000..93973bb9e --- /dev/null +++ b/rinja_derive_standalone/benches/filters.rs @@ -0,0 +1,157 @@ +use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; +use quote::quote; + +criterion_main!(benches); +criterion_group!( + benches, + noop, + no_filters, + few_filters, + all_filters, + some_filters_twice, +); + +fn noop(c: &mut Criterion) { + let ts = quote! { + #[derive(Template)] + #[template(source = "", ext = "html")] + struct Hello; + }; + c.bench_function("noop", |b| { + b.iter_batched( + || ts.clone(), + rinja_derive_standalone::derive_template, + BatchSize::LargeInput, + ); + }); +} + +fn no_filters(c: &mut Criterion) { + let ts = quote! { + #[derive(Template)] + #[template( + source = "\ + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod \ + tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At \ + vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, \ + no sea takimata sanctus est Lorem ipsum dolor sit amet.\ + ", + ext = "html" + )] + struct Hello; + }; + c.bench_function("no_filters", |b| { + b.iter_batched( + || ts.clone(), + rinja_derive_standalone::derive_template, + BatchSize::LargeInput, + ); + }); +} + +fn few_filters(c: &mut Criterion) { + let ts = quote! { + #[derive(Template)] + #[template( + source = "\ + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod \ + tempor invidunt ut labore et dolore {{ user | upper }} erat, sed diam voluptua. At \ + vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, \ + no sea takimata sanctus est {{ user | uppercase | safe }} ipsum dolor sit amet.\ + ", + ext = "html" + )] + struct Hello<'a> { + user: &'a str + } + }; + c.bench_function("few_filters", |b| { + b.iter_batched( + || ts.clone(), + rinja_derive_standalone::derive_template, + BatchSize::LargeInput, + ); + }); +} + +fn all_filters(c: &mut Criterion) { + let ts = quote! { + #[derive(Template)] + #[template( + source = "\ + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, {{ user | capitalize }} \ + sed diam nonumy eirmod tempor invidunt ut labore et dolore {{ user | center(10) }} \ + magna aliquyam erat, sed diam voluptua. At vero eos et accusam {{ user | deref }} \ + et justo duo dolores et ea rebum. Stet clita kasd gubergren, {{ user | escape }} \ + no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem {{ user | e }} \ + ipsum dolor sit amet, consetetur sadipscing elitr, sed {{ user | filesizeformat }} \ + diam nonumy eirmod tempor invidunt ut labore et dolore {{ user | fmt(\":?\") }} \ + magna aliquyam erat, sed diam voluptua. At vero eos {{ \"{:?}\" | format(user) }} \ + et accusam et justo duo dolores et ea rebum. Stet clita {{ user | indent(10) }} \ + kasd gubergren, no sea takimata sanctus {{ [user, user, user] | join(\", \") }} \ + est orem ipsum dolor sit amet. Lorem ipsum dolor sit {{ user | linebreaks }} \ + amet, consetetur sadipscing elitr, sed diam nonumy {{ user | linebreaksbr }} \ + eirmod tempor invidunt ut labore et dolore magna {{ user | paragraphbreaks }} \ + aliquyam erat, sed diam voluptua. At vero eos et accusam et {{ user | lower }} \ + justo duo dolores et ea rebum. Stet clita kasd gubergren, {{ user | lowercase }} \ + no sea takimata sanctus est Lorem ipsum dolor sit amet {{ user | pluralize}}.\n\ + \n\ + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam {{ user | ref }} \ + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam {{ user | safe }} \ + erat, sed diam voluptua. At vero eos et accusam et justo duo {{ user | title }} \ + dolores et ea rebum. Stet clita kasd gubergren, no sea takimata {{ user | trim }} \ + sanctus est Lorem ipsum dolor sit amet. Lorem ipsum {{ user | truncate(10) }} \ + dolor sit amet, consetetur sadipscing elitr, sed diam nonumy {{ user | upper }} \ + eirmod tempor invidunt ut labore et dolore magna aliquyam {{ user | uppercase }} \ + erat, sed diam voluptua. At vero eos et accusam et justo {{ user | urlencode }} \ + duo dolores et ea rebum. Stet clita kasd gubergren, {{ user | urlencode_strict }} \ + no sea takimata sanctus est Lorem ipsum {{ [user, user, user] | join(\", \") }} \ + dolor sit amet. Lorem ipsum dolor sit amet, consetetur {{ user | wordcount }} \ + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut {{ user | custom }} \ + labore et dolore magna aliquyam erat, sed diam {{ user | also_custom(42) }} \ + voluptua. At vero eos et accusam et justo duo {{ user | even::(custom) }} \ + dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est \ + Lorem ipsum dolor sit amet.\ + ", + ext = "html" + )] + struct Hello<'a> { + user: &'a str + } + }; + c.bench_function("all_filters", |b| { + b.iter_batched( + || ts.clone(), + rinja_derive_standalone::derive_template, + BatchSize::LargeInput, + ); + }); +} + +fn some_filters_twice(c: &mut Criterion) { + let ts = quote! { + #[derive(Template)] + #[template( + source = "\ + Lorem ipsum dolor sit amet, consetetur {{ user | capitalize | center(10) }} \ + sadipscing elitr, sed diam nonumy eirmod tempor {{ user | center(10) | deref }} \ + invidunt ut labore et dolore magna aliquyam erat, {{ user | deref | escape }} \ + sed diam voluptua. At vero eos et accusam et justo duo {{ user | escape | e }} \ + dolores et ea rebum. Stet clita kasd gubergren, {{ user | e | filesizeformat }} \ + no sea takimata sanctus est Lorem ipsum {{ user | filesizeformat | fmt(\":?\") }}\ + Lorem ipsum dolor sit amet.\ + ", + ext = "html" + )] + struct Hello<'a> { + user: &'a str + } + }; + c.bench_function("some_filters_twice", |b| { + b.iter_batched( + || ts.clone(), + rinja_derive_standalone::derive_template, + BatchSize::LargeInput, + ); + }); +} diff --git a/testing/tests/ui/escape-filter-invalid-kind.stderr b/testing/tests/ui/escape-filter-invalid-kind.stderr index 7952d6594..9b19ef208 100644 --- a/testing/tests/ui/escape-filter-invalid-kind.stderr +++ b/testing/tests/ui/escape-filter-invalid-kind.stderr @@ -1,4 +1,4 @@ -error: invalid escaper `b"none"`. Expected a string, found a slice +error: expected string literal for `escape` filter, got a slice --> BadEscapeKind.txt:1:14 "b\"none\") }}" --> tests/ui/escape-filter-invalid-kind.rs:5:14 @@ -6,7 +6,7 @@ error: invalid escaper `b"none"`. Expected a string, found a slice 5 | source = r#"{{ "a"|escape(b"none") }}"#, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: invalid escaper `b"none"`. Expected a string, found a CStr +error: expected string literal for `escape` filter, got a CStr --> BadEscapeKind2.txt:1:14 "c\"none\") }}" --> tests/ui/escape-filter-invalid-kind.rs:12:14 diff --git a/testing/tests/ui/json-too-many-args.stderr b/testing/tests/ui/json-too-many-args.stderr index c2e59678c..359b201ff 100644 --- a/testing/tests/ui/json-too-many-args.stderr +++ b/testing/tests/ui/json-too-many-args.stderr @@ -1,4 +1,4 @@ -error: unexpected argument(s) in `json` filter +error: filter `json` expects at most one optional argument, got 2 --> OneTwoThree.txt:1:3 "1|json(2, 3) }}" --> tests/ui/json-too-many-args.rs:6:34