Repository: rinja-rs/rinja (rust)

Issue:
Allow `filesizeformat` to also work on `&i64` (and any integers between any level of reference)

Pull request description:
Relax `filesizeformat` filter requirements
Fixes #215.

Funnily enough, it also allowed to remove one dependency, so yeay! \o/

Patch:
diff --git a/book/src/getting_started.md b/book/src/getting_started.md
index 9a19094a..5eedc330 100644
--- a/book/src/getting_started.md
+++ b/book/src/getting_started.md
@@ -4,7 +4,7 @@ First, add the following to your crate's `Cargo.toml`:
 
 ```toml
 # in section [dependencies]
-rinja = "0.3.5"
+rinja = "0.12.1"
 ```
 
 Now create a directory called `templates` in your crate root.
@@ -43,7 +43,7 @@ First, add this to your `Cargo.toml` instead:
 
 ```toml
 # in section [dependencies]
-rinja_axum = "0.3.5"
+rinja_axum = "0.4.0"
 ```
 
 Then, import from rinja_axum instead of rinja:
diff --git a/rinja/Cargo.toml b/rinja/Cargo.toml
index 98140d43..8c8f0721 100644
--- a/rinja/Cargo.toml
+++ b/rinja/Cargo.toml
@@ -21,11 +21,10 @@ rustdoc-args = ["--generate-link-to-definition", "--cfg=docsrs"]
 maintenance = { status = "actively-developed" }
 
 [features]
-default = ["config", "humansize", "urlencode"]
+default = ["config", "urlencode"]
 full = ["default", "code-in-doc", "serde_json"]
 code-in-doc = ["rinja_derive/code-in-doc"]
 config = ["rinja_derive/config"]
-humansize = ["rinja_derive/humansize", "dep:humansize"]
 serde_json = ["rinja_derive/serde_json", "dep:serde", "dep:serde_json"]
 urlencode = ["rinja_derive/urlencode", "dep:percent-encoding"]
 
@@ -39,7 +38,6 @@ with-warp = ["rinja_derive/with-warp"]
 [dependencies]
 rinja_derive = { version = "=0.3.5", path = "../rinja_derive" }
 
-humansize = { version = "2", optional = true }
 num-traits = { version = "0.2.6", optional = true }
 percent-encoding = { version = "2.1.0", optional = true }
 serde = { version = "1.0", optional = true }
diff --git a/rinja/src/filters/humansize.rs b/rinja/src/filters/humansize.rs
index 587cd44e..bd0cf5ea 100644
--- a/rinja/src/filters/humansize.rs
+++ b/rinja/src/filters/humansize.rs
@@ -1,7 +1,9 @@
 use std::convert::Infallible;
 use std::fmt;
+use std::mem::MaybeUninit;
+use std::str::from_utf8_unchecked;
 
-use humansize::{DECIMAL, ISizeFormatter, ToF64};
+use super::FastWritable;
 
 /// Returns adequate string representation (in KB, ..) of number of bytes
 ///
@@ -21,24 +23,95 @@ use humansize::{DECIMAL, ISizeFormatter, ToF64};
 /// assert_eq!(tmpl.to_string(),  "Filesize: 1.23 MB.");
 /// ```
 #[inline]
-pub fn filesizeformat(b: &impl ToF64) -> Result<FilesizeFormatFilter, Infallible> {
-    Ok(FilesizeFormatFilter(b.to_f64()))
+pub fn filesizeformat(b: f32) -> Result<FilesizeFormatFilter, Infallible> {
+    Ok(FilesizeFormatFilter(b))
 }
 
 #[derive(Debug, Clone, Copy)]
-pub struct FilesizeFormatFilter(f64);
+pub struct FilesizeFormatFilter(f32);
 
 impl fmt::Display for FilesizeFormatFilter {
+    #[inline]
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.write_fmt(format_args!("{}", ISizeFormatter::new(self.0, &DECIMAL)))
+        self.write_into(f)
     }
 }
 
+impl FastWritable for FilesizeFormatFilter {
+    fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
+        if self.0 < 1e3 {
+            (self.0 as u32).write_into(dest)?;
+            dest.write_str(" B")
+        } else if let Some((prefix, factor)) = SI_PREFIXES
+            .iter()
+            .copied()
+            .find_map(|(prefix_factor, max)| (self.0 < max).then_some(prefix_factor))
+        {
+            // u32 is big enough to hold the number 999_999
+            let scaled = (self.0 * factor) as u32;
+            (scaled / 100).write_into(dest)?;
+            dest.write_str(format_frac(&mut MaybeUninit::uninit(), prefix, scaled))
+        } else {
+            too_big(self.0, dest)
+        }
+    }
+}
+
+/// Formats `buffer` to contain the decimal point, decimal places and unit
+fn format_frac(buffer: &mut MaybeUninit<[u8; 8]>, prefix: u8, scaled: u32) -> &str {
+    // LLVM generates better byte code for register sized buffers, so we add some NULs
+    let buffer = buffer.write(*b"..0 kB\0\0");
+    buffer[4] = prefix;
+
+    let frac = scaled % 100;
+    let buffer = if frac == 0 {
+        &buffer[3..6]
+    } else if frac % 10 == 0 {
+        // the decimal separator '.' is already contained in buffer[1]
+        buffer[2] = b'0' + (frac / 10) as u8;
+        &buffer[1..6]
+    } else {
+        // the decimal separator '.' is already contained in buffer[0]
+        buffer[1] = b'0' + (frac / 10) as u8;
+        buffer[2] = b'0' + (frac % 10) as u8;
+        &buffer[0..6]
+    };
+    // SAFETY: we know that the buffer contains only ASCII data
+    unsafe { from_utf8_unchecked(buffer) }
+}
+
+#[cold]
+fn too_big<W: fmt::Write + ?Sized>(value: f32, dest: &mut W) -> fmt::Result {
+    // the value exceeds 999 QB, so we omit the decimal places
+    write!(dest, "{:.0} QB", value / 1e30)
+}
+
+/// `((si_prefix, factor), limit)`, the factor is offset by 10**2 to account for 2 decimal places
+const SI_PREFIXES: &[((u8, f32), f32)] = &[
+    ((b'k', 1e-1), 1e6),
+    ((b'M', 1e-4), 1e9),
+    ((b'G', 1e-7), 1e12),
+    ((b'T', 1e-10), 1e15),
+    ((b'P', 1e-13), 1e18),
+    ((b'E', 1e-16), 1e21),
+    ((b'Z', 1e-19), 1e24),
+    ((b'Y', 1e-22), 1e27),
+    ((b'R', 1e-25), 1e30),
+    ((b'Q', 1e-28), 1e33),
+];
+
 #[test]
+#[allow(clippy::needless_borrows_for_generic_args)]
 fn test_filesizeformat() {
-    assert_eq!(filesizeformat(&0).unwrap().to_string(), "0 B");
-    assert_eq!(filesizeformat(&999u64).unwrap().to_string(), "999 B");
-    assert_eq!(filesizeformat(&1000i32).unwrap().to_string(), "1 kB");
-    assert_eq!(filesizeformat(&1023).unwrap().to_string(), "1.02 kB");
-    assert_eq!(filesizeformat(&1024usize).unwrap().to_string(), "1.02 kB");
+    assert_eq!(filesizeformat(0.).unwrap().to_string(), "0 B");
+    assert_eq!(filesizeformat(999.).unwrap().to_string(), "999 B");
+    assert_eq!(filesizeformat(1000.).unwrap().to_string(), "1 kB");
+    assert_eq!(filesizeformat(1023.).unwrap().to_string(), "1.02 kB");
+    assert_eq!(filesizeformat(1024.).unwrap().to_string(), "1.02 kB");
+    assert_eq!(filesizeformat(1100.).unwrap().to_string(), "1.1 kB");
+    assert_eq!(filesizeformat(9_499_014.).unwrap().to_string(), "9.49 MB");
+    assert_eq!(
+        filesizeformat(954_548_589.2).unwrap().to_string(),
+        "954.54 MB"
+    );
 }
diff --git a/rinja/src/filters/mod.rs b/rinja/src/filters/mod.rs
index c3f36269..9899f225 100644
--- a/rinja/src/filters/mod.rs
+++ b/rinja/src/filters/mod.rs
@@ -12,7 +12,6 @@
 
 mod builtin;
 mod escape;
-#[cfg(feature = "humansize")]
 mod humansize;
 #[cfg(feature = "serde_json")]
 mod json;
@@ -27,7 +26,6 @@ pub use self::escape::{
     AutoEscape, AutoEscaper, Escaper, FastWritable, Html, HtmlSafe, HtmlSafeOutput, MaybeSafe,
     Safe, Text, Unsafe, Writable, WriteWritable, e, escape, safe,
 };
-#[cfg(feature = "humansize")]
 pub use self::humansize::filesizeformat;
 #[cfg(feature = "serde_json")]
 pub use self::json::{AsIndent, json, json_pretty};
diff --git a/rinja/src/lib.rs b/rinja/src/lib.rs
index 1baa77d7..e528d5e3 100644
--- a/rinja/src/lib.rs
+++ b/rinja/src/lib.rs
@@ -72,6 +72,7 @@ pub use rinja_derive::Template;
 #[doc(hidden)]
 pub use crate as shared;
 pub use crate::error::{Error, Result};
+pub use crate::helpers::PrimitiveType;
 
 /// Main `Template` trait; implementations are generally derived
 ///
diff --git a/rinja_actix/Cargo.toml b/rinja_actix/Cargo.toml
index aa71a500..1b25ee27 100644
--- a/rinja_actix/Cargo.toml
+++ b/rinja_actix/Cargo.toml
@@ -24,7 +24,6 @@ default = ["rinja/default"]
 full = ["rinja/full"]
 code-in-doc = ["rinja/code-in-doc"]
 config = ["rinja/config"]
-humansize = ["rinja/humansize"]
 serde_json = ["rinja/serde_json"]
 urlencode = ["rinja/urlencode"]
 
diff --git a/rinja_axum/Cargo.toml b/rinja_axum/Cargo.toml
index 1297a61a..885f537c 100644
--- a/rinja_axum/Cargo.toml
+++ b/rinja_axum/Cargo.toml
@@ -24,7 +24,6 @@ default = ["rinja/default"]
 full = ["rinja/full"]
 code-in-doc = ["rinja/code-in-doc"]
 config = ["rinja/config"]
-humansize = ["rinja/humansize"]
 serde_json = ["rinja/serde_json"]
 urlencode = ["rinja/urlencode"]
 
diff --git a/rinja_derive/Cargo.toml b/rinja_derive/Cargo.toml
index 93f67bb1..2b9dedf9 100644
--- a/rinja_derive/Cargo.toml
+++ b/rinja_derive/Cargo.toml
@@ -20,7 +20,6 @@ proc-macro = true
 [features]
 code-in-doc = ["dep:pulldown-cmark"]
 config = ["dep:serde", "dep:basic-toml", "parser/config"]
-humansize = []
 urlencode = []
 serde_json = []
 with-actix-web = []
diff --git a/rinja_derive/src/generator.rs b/rinja_derive/src/generator.rs
index f7124c24..7f80eb48 100644
--- a/rinja_derive/src/generator.rs
+++ b/rinja_derive/src/generator.rs
@@ -1657,21 +1657,15 @@ impl<'a> Generator<'a> {
         buf: &mut Buffer,
         name: &str,
         args: &[WithSpan<'_, Expr<'_>>],
-        node: &WithSpan<'_, T>,
+        _node: &WithSpan<'_, T>,
     ) -> Result<DisplayWrap, CompileError> {
-        if cfg!(not(feature = "humansize")) {
-            return Err(ctx.generate_error(
-                &format!("the `{name}` filter requires the `humansize` feature to be enabled"),
-                node,
-            ));
-        }
-
         // All filters return numbers, and any default formatted number is HTML safe.
         buf.write(format_args!(
-            "rinja::filters::HtmlSafeOutput(rinja::filters::{name}(",
+            "rinja::filters::HtmlSafeOutput(rinja::filters::{name}(\
+                 rinja::helpers::get_primitive_value(&("
         ));
         self._visit_args(ctx, buf, args)?;
-        buf.write(")?)");
+        buf.write(")) as rinja::helpers::core::primitive::f32)?)");
         Ok(DisplayWrap::Unwrapped)
     }
 
diff --git a/rinja_derive_standalone/Cargo.toml b/rinja_derive_standalone/Cargo.toml
index b3ce0c12..bbf14d0b 100644
--- a/rinja_derive_standalone/Cargo.toml
+++ b/rinja_derive_standalone/Cargo.toml
@@ -23,7 +23,6 @@ __standalone = []
 
 code-in-doc = ["dep:pulldown-cmark"]
 config = ["dep:serde", "dep:basic-toml", "parser/config"]
-humansize = []
 urlencode = []
 serde_json = []
 with-actix-web = []
diff --git a/rinja_parser/src/expr.rs b/rinja_parser/src/expr.rs
index 3aeccc79..210b9946 100644
--- a/rinja_parser/src/expr.rs
+++ b/rinja_parser/src/expr.rs
@@ -130,7 +130,7 @@ impl<'a> Expr<'a> {
         if !is_template_macro {
             // If this is not a template macro, we don't want to parse named arguments so
             // we instead return an error which will allow to continue the parsing.
-            return fail.parse_next(i);
+            return fail(i);
         }
 
         let (_, level) = level.nest(i)?;
@@ -518,7 +518,7 @@ impl<'a> Suffix<'a> {
             if nested == 0 {
                 Ok((&input[last..], ()))
             } else {
-                fail.parse_next(input)
+                fail(input)
             }
         }
 
diff --git a/rinja_parser/src/lib.rs b/rinja_parser/src/lib.rs
index 8e6e633e..362919bc 100644
--- a/rinja_parser/src/lib.rs
+++ b/rinja_parser/src/lib.rs
@@ -346,7 +346,7 @@ fn num_lit<'a>(start: &'a str) -> ParseResult<'a, Num<'a>> {
         start: &'a str,
         i: &'a str,
     ) -> ParseResult<'a, T> {
-        let (i, suffix) = identifier.parse_next(i)?;
+        let (i, suffix) = identifier(i)?;
         if let Some(value) = list
             .iter()
             .copied()
@@ -392,7 +392,7 @@ fn num_lit<'a>(start: &'a str) -> ParseResult<'a, Num<'a>> {
         .parse_next(i)?;
         match (has_dot, has_exp) {
             (Some(_), _) | (_, Some(())) => Ok((i, ())),
-            _ => fail.parse_next(start),
+            _ => fail(start),
         }
     };
 
diff --git a/rinja_parser/src/node.rs b/rinja_parser/src/node.rs
index 6eec10bd..b120d57e 100644
--- a/rinja_parser/src/node.rs
+++ b/rinja_parser/src/node.rs
@@ -109,7 +109,7 @@ impl<'a> Node<'a> {
             "break" => |i, s| Self::r#break(i, s),
             "continue" => |i, s| Self::r#continue(i, s),
             "filter" => |i, s| wrap(Self::FilterBlock, FilterBlock::parse(i, s)),
-            _ => return fail.parse_next(start),
+            _ => return fail(start),
         };
 
         let (i, node) = s.nest(j, |i| func(i, s))?;
@@ -1008,7 +1008,7 @@ impl<'a> Lit<'a> {
         let (i, content) = match content {
             Some("") => {
                 // {block,comment,expr}_start follows immediately.
-                return fail.parse_next(i);
+                return fail(i);
             }
             Some(content) => (i, content),
             None => ("", i), // there is no {block,comment,expr}_start: take everything
@@ -1313,7 +1313,7 @@ pub struct Ws(pub Option<Whitespace>, pub Option<Whitespace>);
 fn end_node<'a, 'g: 'a>(
     node: &'g str,
     expected: &'g str,
-) -> impl Parser<&'a str, &'a str, ErrorContext<'a>> + 'g {
+) -> impl Fn(&'a str) -> ParseResult<'a> + 'g {
     move |start| {
         let (i, actual) = ws(identifier).parse_next(start)?;
         if actual == expected {
@@ -1324,7 +1324,7 @@ fn end_node<'a, 'g: 'a>(
                 start,
             )))
         } else {
-            fail.parse_next(start)
+            fail(start)
         }
     }
 }
diff --git a/rinja_rocket/Cargo.toml b/rinja_rocket/Cargo.toml
index de7aeacc..68da440f 100644
--- a/rinja_rocket/Cargo.toml
+++ b/rinja_rocket/Cargo.toml
@@ -24,7 +24,6 @@ default = ["rinja/default"]
 full = ["rinja/full"]
 code-in-doc = ["rinja/code-in-doc"]
 config = ["rinja/config"]
-humansize = ["rinja/humansize"]
 serde_json = ["rinja/serde_json"]
 urlencode = ["rinja/urlencode"]
 
diff --git a/rinja_warp/Cargo.toml b/rinja_warp/Cargo.toml
index 87e98a11..71d7dacf 100644
--- a/rinja_warp/Cargo.toml
+++ b/rinja_warp/Cargo.toml
@@ -24,7 +24,6 @@ default = ["rinja/default"]
 full = ["rinja/full"]
 code-in-doc = ["rinja/code-in-doc"]
 config = ["rinja/config"]
-humansize = ["rinja/humansize"]
 serde_json = ["rinja/serde_json"]
 urlencode = ["rinja/urlencode"]
 


Test patch:
diff --git a/testing/tests/filters.rs b/testing/tests/filters.rs
index 67c97483..56356d3f 100644
--- a/testing/tests/filters.rs
+++ b/testing/tests/filters.rs
@@ -427,3 +427,18 @@ fn test_linebreaks() {
         "<p>&#60;script&#62;<br/>alert(&#39;Hello, world!&#39;)<br/>&#60;/script&#62;</p>",
     );
 }
+
+// Regression tests for <https://github.com/rinja-rs/rinja/issues/215>.
+#[test]
+fn test_filesizeformat() {
+    #[derive(Template)]
+    #[template(
+        source = r#"{% if let Some(x) = s %}{{x|filesizeformat}}{% endif %}"#,
+        ext = "html"
+    )]
+    struct S {
+        s: Option<u32>,
+    }
+
+    assert_eq!(S { s: Some(12) }.render().unwrap(), "12 B");
+}