//! Functions related to adding and removing indentation from lines of //! text. //! //! The functions here can be used to uniformly indent or dedent //! (unindent) word wrapped lines of text. /// Add prefix to each non-empty line. /// /// ``` /// use textwrap::indent; /// /// assert_eq!(indent(" /// Foo /// Bar /// ", " "), " /// Foo /// Bar /// "); /// ``` /// /// Empty lines (lines consisting only of whitespace) are not indented /// and the whitespace is replaced by a single newline (`\n`): /// /// ``` /// use textwrap::indent; /// /// assert_eq!(indent(" /// Foo /// /// Bar /// \t /// Baz /// ", "->"), " /// ->Foo /// /// ->Bar /// /// ->Baz /// "); /// ``` /// /// Leading and trailing whitespace on non-empty lines is kept /// unchanged: /// /// ``` /// use textwrap::indent; /// /// assert_eq!(indent(" \t Foo ", "->"), "-> \t Foo \n"); /// ``` pub fn indent(s: &str, prefix: &str) -> String { let mut result = String::new(); for line in s.lines() { if line.chars().any(|c| !c.is_whitespace()) { result.push_str(prefix); result.push_str(line); } result.push('\n'); } result } /// Removes common leading whitespace from each line. /// /// This function will look at each non-empty line and determine the /// maximum amount of whitespace that can be removed from all lines: /// /// ``` /// use textwrap::dedent; /// /// assert_eq!(dedent(" /// 1st line /// 2nd line /// 3rd line /// "), " /// 1st line /// 2nd line /// 3rd line /// "); /// ``` pub fn dedent(s: &str) -> String { let mut prefix = ""; let mut lines = s.lines(); // We first search for a non-empty line to find a prefix. for line in &mut lines { let mut whitespace_idx = line.len(); for (idx, ch) in line.char_indices() { if !ch.is_whitespace() { whitespace_idx = idx; break; } } // Check if the line had anything but whitespace if whitespace_idx < line.len() { prefix = &line[..whitespace_idx]; break; } } // We then continue looking through the remaining lines to // possibly shorten the prefix. for line in &mut lines { let mut whitespace_idx = line.len(); for ((idx, a), b) in line.char_indices().zip(prefix.chars()) { if a != b { whitespace_idx = idx; break; } } // Check if the line had anything but whitespace and if we // have found a shorter prefix if whitespace_idx < line.len() && whitespace_idx < prefix.len() { prefix = &line[..whitespace_idx]; } } // We now go over the lines a second time to build the result. let mut result = String::new(); for line in s.lines() { if line.starts_with(&prefix) && line.chars().any(|c| !c.is_whitespace()) { let (_, tail) = line.split_at(prefix.len()); result.push_str(tail); } result.push('\n'); } if result.ends_with('\n') && !s.ends_with('\n') { let new_len = result.len() - 1; result.truncate(new_len); } result } #[cfg(test)] mod tests { use super::*; /// Add newlines. Ensures that the final line in the vector also /// has a newline. fn add_nl(lines: &[&str]) -> String { lines.join("\n") + "\n" } #[test] fn indent_empty() { assert_eq!(indent("\n", " "), "\n"); } #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn indent_nonempty() { let x = vec![" foo", "bar", " baz"]; let y = vec!["// foo", "//bar", "// baz"]; assert_eq!(indent(&add_nl(&x), "//"), add_nl(&y)); } #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn indent_empty_line() { let x = vec![" foo", "bar", "", " baz"]; let y = vec!["// foo", "//bar", "", "// baz"]; assert_eq!(indent(&add_nl(&x), "//"), add_nl(&y)); } #[test] fn dedent_empty() { assert_eq!(dedent(""), ""); } #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn dedent_multi_line() { let x = vec![" foo", " bar", " baz"]; let y = vec![" foo", "bar", " baz"]; assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); } #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn dedent_empty_line() { let x = vec![" foo", " bar", " ", " baz"]; let y = vec![" foo", "bar", "", " baz"]; assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); } #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn dedent_blank_line() { let x = vec![" foo", "", " bar", " foo", " bar", " baz"]; let y = vec!["foo", "", " bar", " foo", " bar", " baz"]; assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); } #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn dedent_whitespace_line() { let x = vec![" foo", " ", " bar", " foo", " bar", " baz"]; let y = vec!["foo", "", " bar", " foo", " bar", " baz"]; assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); } #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn dedent_mixed_whitespace() { let x = vec!["\tfoo", " bar"]; let y = vec!["\tfoo", " bar"]; assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); } #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn dedent_tabbed_whitespace() { let x = vec!["\t\tfoo", "\t\t\tbar"]; let y = vec!["foo", "\tbar"]; assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); } #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn dedent_mixed_tabbed_whitespace() { let x = vec!["\t \tfoo", "\t \t\tbar"]; let y = vec!["foo", "\tbar"]; assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); } #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn dedent_mixed_tabbed_whitespace2() { let x = vec!["\t \tfoo", "\t \tbar"]; let y = vec!["\tfoo", " \tbar"]; assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); } #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn dedent_preserve_no_terminating_newline() { let x = vec![" foo", " bar"].join("\n"); let y = vec!["foo", " bar"].join("\n"); assert_eq!(dedent(&x), y); } }