diff --git a/docs/content/docs/_index.md b/docs/content/docs/_index.md index ac957890..48dafed8 100644 --- a/docs/content/docs/_index.md +++ b/docs/content/docs/_index.md @@ -867,6 +867,9 @@ Note that only whitespace between successive opening tags and successive closing Also note that if the template you are using it in is automatically escaped, you will need to call the `safe` filter after `spaceless`. +#### indent +Indents a string by injecting a prefix at the start of each line. The `prefix` argument (default 4 spaces) specifies the prefix to insert per line. If the `first` argument (default false) is set true spaces are inserted for the first line. If the `blank` argument (default false) is set true spaces are inserted for blank/whitespace lines. + #### striptags Tries to remove HTML tags from input. Does not guarantee well formed output if input is not valid HTML. diff --git a/src/builtins/filters/string.rs b/src/builtins/filters/string.rs index 1fc02b27..d874bd6d 100644 --- a/src/builtins/filters/string.rs +++ b/src/builtins/filters/string.rs @@ -262,6 +262,57 @@ pub fn linebreaksbr(value: &Value, _: &HashMap) -> Result Ok(to_value(s.replace("\r\n", "
").replace('\n', "
")).unwrap()) } +/// Indents a string by the specified width. +/// +/// # Arguments +/// +/// * `value` - The string to indent. +/// * `args` - A set of key/value arguments that can take the following +/// keys. +/// * `prefix` - The prefix used for indentation. The default value is 4 spaces. +/// * `first` - True indents the first line. The default is false. +/// * `blank` - True indents blank lines. The default is false. +/// +pub fn indent(value: &Value, args: &HashMap) -> Result { + let s = try_get_value!("indent", "value", String, value); + + let prefix = match args.get("prefix") { + Some(p) => try_get_value!("indent", "prefix", String, p), + None => " ".to_string(), + }; + let first = match args.get("first") { + Some(f) => try_get_value!("indent", "first", bool, f), + None => false, + }; + let blank = match args.get("blank") { + Some(b) => try_get_value!("indent", "blank", bool, b), + None => false, + }; + + // Attempt to pre-allocate enough space to prevent additional allocations/copies + let mut out = String::with_capacity( + s.len() + (prefix.len() * (s.chars().filter(|&c| c == '\n').count() + 1)), + ); + let mut first_pass = true; + + for line in s.lines() { + if first_pass { + if first { + out.push_str(&prefix); + } + first_pass = false; + } else { + out.push('\n'); + if blank || !line.trim_start().is_empty() { + out.push_str(&prefix); + } + } + out.push_str(line); + } + + Ok(to_value(&out).unwrap()) +} + /// Removes html tags from string pub fn striptags(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("striptags", "value", String, value); @@ -671,6 +722,25 @@ mod tests { } } + #[test] + fn test_indent_defaults() { + let args = HashMap::new(); + let result = indent(&to_value("one\n\ntwo\nthree").unwrap(), &args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), to_value("one\n\n two\n three").unwrap()); + } + + #[test] + fn test_indent_args() { + let mut args = HashMap::new(); + args.insert("first".to_string(), to_value(true).unwrap()); + args.insert("prefix".to_string(), to_value(" ").unwrap()); + args.insert("blank".to_string(), to_value(true).unwrap()); + let result = indent(&to_value("one\n\ntwo\nthree").unwrap(), &args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), to_value(" one\n \n two\n three").unwrap()); + } + #[test] fn test_striptags() { let tests = vec![ diff --git a/src/tera.rs b/src/tera.rs index 4f27c1e1..b5876d63 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -578,6 +578,7 @@ impl Tera { self.register_filter("capitalize", string::capitalize); self.register_filter("title", string::title); self.register_filter("linebreaksbr", string::linebreaksbr); + self.register_filter("indent", string::indent); self.register_filter("striptags", string::striptags); self.register_filter("spaceless", string::spaceless); #[cfg(feature = "urlencode")]