Repository: mitsuhiko/minijinja
Pull request #649 (2024-11-24 16:45:37)

added wordcount filter

yacir: Nice!
However, I noticed that the implementation doesn’t fully match the Jinja2 result.

Using split_whitespace might fail, as Jinja2 relies on the \w+ regex. Here’s an example of a mismatched result:

```python
{{'hello-world'|wordcount}} # -> 2
{{'hello@world'|wordcount}} # -> 2
```

```rust
"{}", "hello@world'".split_whitespace().count() // -> 1
"{}", "hello@world'".split_whitespace().count() // -> 1
```
jqnatividad: That @yacir for catching that as I just went for the naive implementation and didn't check Jinja2.

Patch:
diff --git a/minijinja-contrib/src/filters/mod.rs b/minijinja-contrib/src/filters/mod.rs
index a303ff2..6b96685 100644
--- a/minijinja-contrib/src/filters/mod.rs
+++ b/minijinja-contrib/src/filters/mod.rs
@@ -214,3 +214,27 @@ pub fn truncate(state: &State, value: Value, kwargs: Kwargs) -> Result<String, E
     result.push_str(end);
     Ok(result)
 }
+
+/// Counts the words in a string.
+///
+/// ```jinja
+/// {{ "Hello world!"|wordcount }}
+/// ```
+pub fn wordcount(value: Value) -> Result<Value, Error> {
+    let s = value.as_str().unwrap_or_default();
+    let mut count: u32 = 0;
+    let mut in_word = false;
+
+    // Iterate through characters, counting transitions from non-word to word chars
+    for c in s.chars() {
+        let is_word_char = c.is_alphanumeric() || c == '_';
+        if is_word_char && !in_word {
+            count += 1;
+            in_word = true;
+        } else if !is_word_char {
+            in_word = false;
+        }
+    }
+
+    Ok(Value::from(count))
+}


Test patch:
diff --git a/minijinja-contrib/tests/filters.rs b/minijinja-contrib/tests/filters.rs
index 0e2df55..4184472 100644
--- a/minijinja-contrib/tests/filters.rs
+++ b/minijinja-contrib/tests/filters.rs
@@ -1,5 +1,5 @@
 use minijinja::{context, Environment};
-use minijinja_contrib::filters::pluralize;
+use minijinja_contrib::filters::{pluralize, wordcount};
 use similar_asserts::assert_eq;
 
 #[test]
@@ -202,3 +202,68 @@ fn test_truncate() {
         "invalid operation: expected length >= 3, got 1 (in <string>:1)"
     );
 }
+
+#[test]
+fn test_wordcount() {
+    let mut env = Environment::new();
+    env.add_filter("wordcount", wordcount);
+
+    assert_eq!(
+        env.render_str(
+            "{{ text|wordcount }}",
+            context! {
+                text => "Hello world! How are you?"
+            }
+        )
+        .unwrap(),
+        "5"
+    );
+
+    // Test empty string
+    assert_eq!(
+        env.render_str(
+            "{{ text|wordcount }}",
+            context! {
+                text => ""
+            }
+        )
+        .unwrap(),
+        "0"
+    );
+
+    // Test multiple whitespace
+    assert_eq!(
+        env.render_str(
+            "{{ text|wordcount }}",
+            context! {
+                text => "Hello    world!   Test"
+            }
+        )
+        .unwrap(),
+        "3"
+    );
+
+    // Test other word separators
+    assert_eq!(
+        env.render_str(
+            "{{ text|wordcount }}",
+            context! {
+                text => "hello-again@world! It's_me!"
+            }
+        )
+        .unwrap(),
+        "5"
+    );
+
+    // Test multiple other word separators
+    assert_eq!(
+        env.render_str(
+            "{{ text|wordcount }}",
+            context! {
+                text => "hello--again@-!world"
+            }
+        )
+        .unwrap(),
+        "3"
+    );
+}