From b4dfbbe703357d86043e19162ffa8873b1534eab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Fri, 24 Jan 2025 23:22:30 +0100 Subject: [PATCH] derive: move filters into its own file --- rinja_derive/src/generator.rs | 1 + rinja_derive/src/generator/expr.rs | 683 +------------------------- rinja_derive/src/generator/filters.rs | 654 ++++++++++++++++++++++++ 3 files changed, 669 insertions(+), 669 deletions(-) create mode 100644 rinja_derive/src/generator/filters.rs diff --git a/rinja_derive/src/generator.rs b/rinja_derive/src/generator.rs index 3318b45a..372de0cd 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 02343b46..68305f07 100644 --- a/rinja_derive/src/generator/expr.rs +++ b/rinja_derive/src/generator/expr.rs @@ -1,19 +1,15 @@ use std::borrow::Cow; -use std::fmt; 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::{CompileError, MsgValidEscapers}; impl<'a> Generator<'a, '_> { pub(crate) fn visit_expr_root( @@ -240,521 +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 { - type Filter = for<'a> fn( - this: &mut Generator<'a, '_>, - ctx: &Context<'_>, - buf: &mut Buffer, - name: &str, - args: &[WithSpan<'_, Expr<'a>>], - generics: &[WithSpan<'_, TyGenerics<'_>>], - node: Span<'_>, - ) -> Result; - - macro_rules! filters_no_generic { - ($($wrapper:ident => $fn:ident),+ $(,)?) => { $( - fn $wrapper<'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)?; - this.$fn(ctx, buf, name, args, node) - } - )+ }; - } - - macro_rules! filters_tbl_sub { - ( - $dest:ident[$len:literal] = { - $($($name:literal)|+ => $wrapper:ident),+ $(,)? - } - ) => {{ - const FILTERS: &[(&str, Filter)] = - &[ $( $( ($name, $wrapper) ),+ ),+ ]; - - let mut i = 0; - while i < FILTERS.len() { - if FILTERS[i].0.len() != $len { - panic!(); - } - i += 1; - } - - $dest[$len] = FILTERS; - }}; - } - - macro_rules! filters_tbl { - ( [$limit:literal] $($len:literal => $tt:tt),+ $(,)? ) => {{ - let mut filters: [&[(&str, Filter)]; {$limit + 2}] = - [&[]; {$limit + 2}]; - $( filters_tbl_sub!(filters[$len] = $tt); )+ - filters - }} - } - - filters_no_generic! { - deref => _visit_deref_filter, - escape => _visit_escape_filter, - humansize => _visit_humansize_filter, - fmt => _visit_fmt_filter, - format => _visit_format_filter, - join => _visit_join_filter, - json => _visit_json_filter, - linebreaks => _visit_linebreaks_filters, - pluralize => _visit_pluralize_filter, - r#ref => _visit_ref_filter, - safe => _visit_safe_filter, - urlencode => _visit_urlencode_filter, - builtin => _visit_builtin_filter, - } - - 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 custom<'a>( - this: &mut Generator<'a, '_>, - ctx: &Context<'_>, - buf: &mut Buffer, - name: &str, - args: &[WithSpan<'_, Expr<'a>>], - generics: &[WithSpan<'_, TyGenerics<'_>>], - _node: Span<'_>, - ) -> Result { - this._visit_custom_filter(ctx, buf, name, args, generics) - } - - const FILTERS_BY_NAME_LEN: &[&[(&str, Filter)]] = &filters_tbl! { - [16] - 1 => { - "e" => escape, - }, - 3 => { - "fmt" => fmt, - "ref" => r#ref, - }, - 4 => { - "join" => join, - "json" => json, - "safe" => safe, - "trim" => builtin, - }, - 5 => { - "deref" => deref, - "value" => value, - "lower" | "title" | "upper" => builtin, - }, - 6 => { - "escape" => escape, - "format" => format, - "tojson" => json, - "center" | "indent" => builtin, - }, - 8 => { - "truncate" => builtin, - }, - 9 => { - "pluralize" => pluralize, - "urlencode" => urlencode, - "lowercase" | "uppercase" | "wordcount" => builtin, - }, - 10 => { - "capitalize" => builtin, - "linebreaks" => linebreaks, - }, - 12 => { - "linebreaksbr" => linebreaks, - }, - 14 => { - "filesizeformat" => humansize, - }, - 15 => { - "paragraphbreaks" => linebreaks, - }, - 16 => { - "urlencode_strict" => urlencode, - }, - }; - - let filter = FILTERS_BY_NAME_LEN.get(name.len()) - .and_then(|&filters| { - filters.iter().find_map(|&(key, filter)| (key == name).then_some(filter)) - }) - .unwrap_or(custom); - filter(self, ctx, buf, name, args, generics, node) - } - - #[inline] - fn _visit_custom_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - name: &str, - args: &[WithSpan<'_, Expr<'a>>], - generics: &[WithSpan<'_, TyGenerics<'_>>], - ) -> Result { - 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) - } - - #[inline] - fn _visit_builtin_filter( - &mut self, - 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}(")); - self._visit_args(ctx, buf, args)?; - buf.write(")?"); - Ok(DisplayWrap::Unwrapped) - } - - #[inline] - fn _visit_urlencode_filter( - &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, - )); - } - - 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}(", - )); - self._visit_arg(ctx, buf, arg)?; - buf.write(")?)"); - Ok(DisplayWrap::Unwrapped) - } - - #[inline] - fn _visit_humansize_filter( - &mut self, - 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(&(" - )); - self._visit_arg(ctx, buf, arg)?; - buf.write(")) as rinja::helpers::core::primitive::f32)?)"); - Ok(DisplayWrap::Unwrapped) - } - - #[inline] - fn _visit_pluralize_filter( - &mut self, - 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 }; - 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) - } - - #[inline] - 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)?; - let arg = get_filter_argument(ctx, name, args, node)?; - buf.write(format_args!( - "rinja::filters::{name}(&(&&rinja::filters::AutoEscaper::new(&(", - )); - self._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) - } - - #[inline] - fn _visit_ref_filter( - &mut self, - 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('&'); - self.visit_expr(ctx, buf, arg)?; - Ok(DisplayWrap::Unwrapped) - } - - #[inline] - fn _visit_deref_filter( - &mut self, - 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('*'); - self.visit_expr(ctx, buf, arg)?; - Ok(DisplayWrap::Unwrapped) - } - - #[inline] - fn _visit_json_filter( - &mut self, - 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}(")); - self._visit_args(ctx, buf, args)?; - buf.write(")?"); - Ok(DisplayWrap::Unwrapped) - } - - #[inline] - fn _visit_safe_filter( - &mut self, - 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("); - self._visit_arg(ctx, buf, arg)?; - buf.write(format_args!(", {})?", self.input.escaper)); - Ok(DisplayWrap::Wrapped) - } - - #[inline] - fn _visit_escape_filter( - &mut self, - ctx: &Context<'_>, - buf: &mut Buffer, - name: &str, - args: &[WithSpan<'_, Expr<'a>>], - node: Span<'_>, - ) -> Result { - let (arg, escaper) = match args { - [arg] => (arg, self.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 = self - .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(&self.input.config.escapers), - ), - node, - ) - })?; - (arg, escaper) - } - args => return Err(unexpected_filter_arguments(ctx, name, args, node, 1)), - }; - buf.write("rinja::filters::escape("); - self._visit_arg(ctx, buf, arg)?; - buf.write(format_args!(", {escaper})?")); - Ok(DisplayWrap::Wrapped) - } - - #[inline] - fn _visit_format_filter( - &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 let [fmt, args @ ..] = args { - if let Expr::StrLit(fmt) = &**fmt { - buf.write("rinja::helpers::alloc::format!("); - self.visit_str_lit(buf, fmt); - if !args.is_empty() { - buf.write(','); - self._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)) - } - - #[inline] - fn _visit_fmt_filter( - &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 let [arg, fmt] = args { - if let Expr::StrLit(fmt) = &**fmt { - buf.write("rinja::helpers::alloc::format!("); - self.visit_str_lit(buf, fmt); - buf.write(','); - self._visit_arg(ctx, buf, arg)?; - buf.write(')'); - return Ok(DisplayWrap::Unwrapped); - } - } - Err(ctx.generate_error(r#"use filter fmt like `value|fmt("{:?}")`"#, node)) - } - - #[inline] - fn _visit_join_filter( - &mut self, - 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(", &"); - } - 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, @@ -785,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, @@ -800,7 +282,7 @@ impl<'a> Generator<'a, '_> { Ok(()) } - fn _visit_arg( + pub(super) fn _visit_arg( &mut self, ctx: &Context<'_>, buf: &mut Buffer, @@ -842,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, @@ -899,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; } @@ -1169,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()); } @@ -1293,144 +779,3 @@ impl<'a> Generator<'a, '_> { } } } - -#[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 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 00000000..75287331 --- /dev/null +++ b/rinja_derive/src/generator/filters.rs @@ -0,0 +1,654 @@ +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 { + let filter = FILTERS_BY_NAME_LEN + .get(name.len()) + .and_then(|&filters| { + filters + .iter() + .find_map(|&(key, filter)| (key == name).then_some(filter)) + }) + .unwrap_or(custom); + filter(self, ctx, buf, name, args, generics, node) + } +} + +/// A map `name.len() => &[(key, Filter)]`. Assume that the entries are unsorted. +const FILTERS_BY_NAME_LEN: &[&[(&str, Filter)]] = &{ + /// Assigns to `$dest[$len] = &[(key, Filter)]`. + /// It is checked that the entries have the right length. + macro_rules! filters_tbl_sub { + ( + $dest:ident[$len:literal] = { + $($($name:literal)|+ => $wrapper:ident),+ $(,)? + } + ) => {{ + const FILTERS: &[(&str, Filter)] = + &[ $( $( ($name, $wrapper) ),+ ),+ ]; + + let mut i = 0; + while i < FILTERS.len() { + if FILTERS[i].0.len() != $len { + panic!(); + } + i += 1; + } + + $dest[$len] = FILTERS; + }}; + } + + /// Generate a `&[&[(&str, Filter)]; {$limit + 2}]` map. + /// It is safe to call the macro with a bad `limit`: + /// * If it is too low, the compilation will fail + /// * If it is too high, the generated code will be slightly worse. + macro_rules! filters_tbl { + ( [$limit:literal] $($len:literal => $tt:tt),+ $(,)? ) => {{ + let mut filters: [&[(&str, Filter)]; {$limit + 2}] = + [&[]; {$limit + 2}]; + $( filters_tbl_sub!(filters[$len] = $tt); )+ + filters + }} + } + + /// Wraps and shadows `$fn` with a function that checks that no generics were suppied, + /// 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, + } + + filters_tbl! { + [16] + 1 => { + "e" => escape, + }, + 3 => { + "fmt" => fmt, + "ref" => r#ref, + }, + 4 => { + "join" => join, + "json" => json, + "safe" => safe, + "trim" => builtin, + }, + 5 => { + "deref" => deref, + "value" => value, + "lower" | "title" | "upper" => builtin, + }, + 6 => { + "escape" => escape, + "format" => format, + "tojson" => json, + "center" | "indent" => builtin, + }, + 8 => { + "truncate" => builtin, + }, + 9 => { + "pluralize" => pluralize, + "urlencode" => urlencode, + "lowercase" | "uppercase" | "wordcount" => builtin, + }, + 10 => { + "capitalize" => builtin, + "linebreaks" => linebreaks, + }, + 12 => { + "linebreaksbr" => linebreaks, + }, + 14 => { + "filesizeformat" => humansize, + }, + 15 => { + "paragraphbreaks" => linebreaks, + }, + 16 => { + "urlencode_strict" => urlencode, + }, + } +}; + +type Filter = for<'a> fn( + this: &mut Generator<'a, '_>, + ctx: &Context<'_>, + buf: &mut Buffer, + name: &str, + args: &[WithSpan<'_, Expr<'a>>], + generics: &[WithSpan<'_, TyGenerics<'_>>], + node: Span<'_>, +) -> Result; + +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; + } +}