From 5c65cae7b0cfb5ef4b2d0a642a94b85e08657dc6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 1 Dec 2024 10:10:40 +0100 Subject: [PATCH] Refactor and feature gate filter --- CHANGELOG.md | 1 + minijinja-cli/Cargo.toml | 2 +- minijinja-contrib/Cargo.toml | 4 +- minijinja-contrib/src/filters/mod.rs | 61 ++++++++++++++++------------ minijinja-contrib/src/lib.rs | 4 ++ minijinja-contrib/tests/filters.rs | 5 ++- 6 files changed, 47 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd2d7b43..ebab82f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to MiniJinja are documented here. - Added `sum` filter. #648 - Added `truncate` filter to `minijinja-contrib`. #647 - Added `wordcount` filter to `minijinja-contrib`. #649 +- Added `wordwrap` filter to `minijinja-contrib`. #651 ## 2.5.0 diff --git a/minijinja-cli/Cargo.toml b/minijinja-cli/Cargo.toml index e6b3f910..d6521e45 100644 --- a/minijinja-cli/Cargo.toml +++ b/minijinja-cli/Cargo.toml @@ -46,7 +46,7 @@ minijinja = { version = "=2.5.0", path = "../minijinja", features = [ "custom_syntax", "loop_controls" ] } -minijinja-contrib = { version = "=2.5.0", optional = true, path = "../minijinja-contrib", features = ["pycompat", "datetime", "timezone", "rand"] } +minijinja-contrib = { version = "=2.5.0", optional = true, path = "../minijinja-contrib", features = ["pycompat", "datetime", "timezone", "rand", "unicode_wordwrap"] } rustyline = { version = "14.0.0", optional = true } serde = { version = "1.0.183", features = ["derive", "rc"] } serde_json = "1.0.105" diff --git a/minijinja-contrib/Cargo.toml b/minijinja-contrib/Cargo.toml index 1ad0e21b..82786499 100644 --- a/minijinja-contrib/Cargo.toml +++ b/minijinja-contrib/Cargo.toml @@ -21,12 +21,14 @@ pycompat = ["minijinja/builtins"] datetime = ["time"] timezone = ["time-tz"] rand = ["dep:rand"] +wordwrap = ["dep:textwrap"] +unicode_wordwrap = ["wordwrap", "textwrap/unicode-linebreak", "textwrap/unicode-width"] [dependencies] minijinja = { version = "2.5.0", path = "../minijinja", default-features = false } rand = { version = "0.8.5", optional = true, default-features = false, features = ["std", "std_rng", "small_rng"] } serde = "1.0.164" -textwrap = "0.16.1" +textwrap = { version = "0.16.1", optional = true, default-features = false, features = ["smawk"] } time = { version = "0.3.35", optional = true, features = ["serde", "formatting", "parsing"] } time-tz = { version = "1.0.3", features = ["db"], optional = true } diff --git a/minijinja-contrib/src/filters/mod.rs b/minijinja-contrib/src/filters/mod.rs index 5c9e4eb4..9cdf001c 100644 --- a/minijinja-contrib/src/filters/mod.rs +++ b/minijinja-contrib/src/filters/mod.rs @@ -3,7 +3,6 @@ use std::convert::TryFrom; use minijinja::value::{Kwargs, Value, ValueKind}; use minijinja::State; use minijinja::{Error, ErrorKind}; -use textwrap::{wrap, Options as WrapOptions, WordSplitter}; #[cfg(feature = "datetime")] mod datetime; @@ -242,16 +241,22 @@ pub fn wordcount(value: Value) -> Result { /// Wrap a string to the given width. /// -/// Parameters: -/// - s: Original text to wrap -/// - width: Maximum length of wrapped lines (default: 79) -/// - break_long_words: If a word is longer than width, break it across lines (default: true) -/// - break_on_hyphens: If a word contains hyphens, it may be split across lines (default: true) -/// - wrapstring: String to join each wrapped line (default: newline) +/// By default this filter is not unicode aware (feature = `wordwrap`) but when the unicode +/// feature is enabled (`unicode_wordwrap`) then it becomes so. It's implemented on top of +/// the `textwrap` crate. +/// +/// **Keyword arguments:** +/// +/// - `width`: Maximum length of wrapped lines (default: 79) +/// - `break_long_words`: If a word is longer than width, break it across lines (default: true) +/// - `break_on_hyphens`: If a word contains hyphens, it may be split across lines (default: true) +/// - `wrapstring`: String to join each wrapped line (default: newline) +#[cfg(feature = "wordwrap")] +#[cfg_attr(docsrs, doc(any(cfg(feature = "wordwrap"), cfg = "unicode_wordwrap")))] pub fn wordwrap(value: Value, kwargs: Kwargs) -> Result { + use textwrap::{wrap, Options as WrapOptions, WordSplitter}; let s = value.as_str().unwrap_or_default(); - // Extract kwargs with defaults let width = kwargs.get::>("width")?.unwrap_or(79); let break_long_words = kwargs .get::>("break_long_words")? @@ -260,6 +265,7 @@ pub fn wordwrap(value: Value, kwargs: Kwargs) -> Result { .get::>("break_on_hyphens")? .unwrap_or(true); let wrapstring = kwargs.get::>("wrapstring")?.unwrap_or("\n"); + kwargs.assert_all_used()?; let mut options = WrapOptions::new(width).break_words(break_long_words); @@ -269,26 +275,27 @@ pub fn wordwrap(value: Value, kwargs: Kwargs) -> Result { // Handle empty/whitespace-only input if s.trim().is_empty() { - return Ok(Value::from(s)); + return Ok(Value::from("")); } - // Split input into paragraphs on existing newlines - let paragraphs: Vec<&str> = s.split('\n').collect(); - - // Wrap each paragraph separately - let wrapped: Vec = paragraphs - .iter() - .map(|&p| { - if p.trim().is_empty() { - // Preserve empty lines - String::new() - } else { - // Wrap the paragraph - wrap(p, &options).join(wrapstring) + // Process paragraphs sequentially into final string + Ok(Value::from(s.lines().enumerate().fold( + String::new(), + |mut acc, (i, p)| { + if i > 0 { + acc.push_str(wrapstring); } - }) - .collect(); - - // Join paragraphs with newlines - Ok(Value::from(wrapped.join("\n"))) + if !p.trim().is_empty() { + // Wrap the paragraph and join with wrapstring + let wrapped = wrap(p, &options); + for (j, line) in wrapped.iter().enumerate() { + if j > 0 { + acc.push_str(wrapstring); + } + acc.push_str(line); + } + } + acc + }, + ))) } diff --git a/minijinja-contrib/src/lib.rs b/minijinja-contrib/src/lib.rs index c5f40636..e56ddbcc 100644 --- a/minijinja-contrib/src/lib.rs +++ b/minijinja-contrib/src/lib.rs @@ -36,6 +36,10 @@ pub fn add_to_environment(env: &mut Environment) { env.add_filter("filesizeformat", filters::filesizeformat); env.add_filter("truncate", filters::truncate); env.add_filter("wordcount", filters::wordcount); + #[cfg(feature = "wordwrap")] + { + env.add_filter("wordwrap", filters::wordwrap); + } #[cfg(feature = "datetime")] { env.add_filter("datetimeformat", filters::datetimeformat); diff --git a/minijinja-contrib/tests/filters.rs b/minijinja-contrib/tests/filters.rs index bdc1ed96..3649b8d8 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, wordcount, wordwrap}; +use minijinja_contrib::filters::{pluralize, wordcount}; use similar_asserts::assert_eq; #[test] @@ -269,7 +269,10 @@ fn test_wordcount() { } #[test] +#[cfg(feature = "wordwrap")] fn test_wordwrap() { + use minijinja_contrib::filters::wordwrap; + let mut env = minijinja::Environment::new(); env.add_filter("wordwrap", wordwrap);