diff options
| author | Thomas Otto <th1000s@posteo.net> | 2024-07-29 22:51:47 +0200 |
|---|---|---|
| committer | Dan Davison <dandavison7@gmail.com> | 2024-08-01 21:40:55 -0400 |
| commit | 6ef193250ae7464eb0aacd2216f868f05a914bad (patch) | |
| tree | a8886110ee1904907341e5f96d48127cedb75e5f | |
| parent | 546e0ed412e1cdfc08d623b692604a228c538073 (diff) | |
Add wrap function for --help output
Unicode and somewhat ANSI aware, can add indentation (skippable) and
can also skip wrapping lines by using configurable magic prefixes.
| -rw-r--r-- | src/utils/helpwrap.rs | 348 | ||||
| -rw-r--r-- | src/utils/mod.rs | 1 |
2 files changed, 349 insertions, 0 deletions
diff --git a/src/utils/helpwrap.rs b/src/utils/helpwrap.rs new file mode 100644 index 0000000..24191d2 --- /dev/null +++ b/src/utils/helpwrap.rs @@ -0,0 +1,348 @@ +#![allow(clippy::comparison_to_empty)] // no_indent != "", instead of !no_indent.is_empty() + +use crate::ansi::measure_text_width; + +/// Wrap `text` at spaces ('` `') to fit into `width`. If `indent_with` is non-empty, indent +/// each line with this string. If a line from `text` starts with `no_indent`, do not indent. +/// If a line starts with `no_wrap`, do not wrap (empty `no_indent`/`no_wrap` have no effect). +/// If both "magic prefix" markers are used, `no_indent` must be first. +/// Takes unicode and ANSI into account when calculating width, but won't wrap ANSI correctly. +/// Removes trailing spaces. Leading spaces or enumerations with '- ' continue the indentation on +/// the wrapped line. +/// Example: +/// ``` +/// let wrapped = wrap("ab cd ef\n!NI!123\n|AB CD EF GH\n!NI!|123 456 789", 7, "_", "!NI!", "|"); +/// assert_eq!(wrapped, "\ +/// _ab cd\n\ +/// _ef\n\ +/// 123\n\ +/// _AB CD EF GH\n\ +/// 123 456 789\n\ +/// "); +/// ``` +pub fn wrap(text: &str, width: usize, indent_with: &str, no_indent: &str, no_wrap: &str) -> String { + let mut result = String::with_capacity(text.len()); + let indent_len = measure_text_width(indent_with); + + for line in text.lines() { + let line = line.trim_end_matches(' '); + + let (line, indent) = + if let (Some(line), true) = (line.strip_prefix(no_indent), no_indent != "") { + (line, "") + } else { + result.push_str(indent_with); + (line, indent_with) + }; + + if let (Some(line), true) = (line.strip_prefix(no_wrap), no_wrap != "") { + result.push_str(line); + } else { + // `"foo bar end".split_inclusive(' ')` => `["foo ", "bar ", " ", " ", "end"]` + let mut wordit = line.split_inclusive(' '); + let mut curr_len = indent_len; + + if let Some(word) = wordit.next() { + result.push_str(word); + curr_len += measure_text_width(word); + } + + while let Some(mut word) = wordit.next() { + let word_len = measure_text_width(word); + if curr_len + word_len == width + 1 && word.ends_with(' ') { + // If just ' ' is over the limit, let the next word trigger the overflow. + } else if curr_len + word_len > width { + // Remove any trailing whitespace: + let pos = result.trim_end_matches(' ').len(); + result.truncate(pos); + + result.push('\n'); + + // Do not count spaces, skip until next proper word is found. + if word == " " { + for nextword in wordit.by_ref() { + word = nextword; + if word != " " { + break; + } + } + } + + // Re-calculates indent for each wrapped line. Could be done only once, maybe + // after an early return which just uses .len() (works for fullwidth chars). + + // If line started with spaces, indent by that much again. + let (indent, space_pos) = + if let Some(space_prefix_len) = line.find(|c: char| c != ' ') { + ( + format!("{}{}", indent, " ".repeat(space_prefix_len)), + space_prefix_len, + ) + } else { + debug_assert!(false, "line.trim_end_matches() missing?"); + (indent.to_string(), 0) + }; + + // If line started with '- ', treat it as a bullet point and increase indentation + let indent = if line[space_pos..].starts_with("- ") { + format!("{}{}", indent, " ") + } else { + indent + }; + + result.push_str(&indent); + curr_len = measure_text_width(&indent); + } + curr_len += word_len; + result.push_str(word); + } + } + let pos = result.trim_end_matches(' ').len(); + result.truncate(pos); + result.push('\n'); + } + + #[cfg(test)] + if result.find("no-sanity").is_none() { + // sanity check + let stripped_input = text + .replace(" ", "") + .replace("\n", "") + .replace(no_wrap, "") + .replace(no_indent, ""); + let stripped_output = result + .replace(" ", "") + .replace("\n", "") + .replace(indent_with, ""); + assert_eq!(stripped_input, stripped_output); + } + + result +} + +#[cfg(test)] +mod test { + use super::*; + use insta::assert_snapshot; + + #[test] + fn simple_ascii_can_not_split() { + let input = "000 123456789 abcdefghijklmnopqrstuvwxyz ok"; + let result = wrap(input, 5, "", "", ""); + assert_snapshot!(result, @r###" + 000 + 123456789 + abcdefghijklmnopqrstuvwxyz + ok + "###); + } + + #[test] + fn simple_ascii_just_whitespace() { + let input = " \n \n \n \n \n \n"; + let result = wrap(input, 3, "__", "", ""); + assert_snapshot!(result, @r###" + __ + __ + __ + __ + __ + __ + "###); + let result = wrap(input, 3, "", "", ""); + assert_eq!(result, "\n\n\n\n\n\n"); + } + + #[test] + fn simple_ascii_can_not_split_plus_whitespace() { + let input = "000 123456789 abcdefghijklmnopqrstuvwxyz ok"; + let result = wrap(input, 5, "", "", ""); + assert_snapshot!(result, @r###" + 000 + 123456789 + abcdefghijklmnopqrstuvwxyz + ok + "###); + } + + #[test] + fn simple_ascii_keep_leading_input_indent() { + let input = "abc\n Def ghi jkl mno pqr stuv xyz\n Abc def ghijklm\nok"; + let result = wrap(input, 10, "_", "", ""); + assert_snapshot!(result, @r###" + _abc + _ Def ghi + _ jkl mno + _ pqr + _ stuv + _ xyz + _ Abc + _ def + _ ghijklm + _ok + "###); + } + + #[test] + fn simple_ascii_indent_and_bullet_points() { + let input = "- ABC ABC abc\n def ghi - jkl\n - 1 22 3 4 55 6 7 8 9\n - 1 22 3 4 55 6 7 8 9\n!- 0 0 0 0 0 0 0 \n"; + let result = wrap(input, 10, "", "!", ""); + assert_snapshot!(result, @r###" + - ABC ABC + abc + def ghi + - jkl + - 1 22 3 + 4 55 6 + 7 8 9 + - 1 22 + 3 4 + 55 6 + 7 8 + 9 + - 0 0 0 0 + 0 0 0 + "###); + } + + #[test] + fn simple_ascii_all_overlong_after_indent() { + let input = "0000 1111 2222"; + let result = wrap(input, 5, "__", "", ""); + assert_snapshot!(result, @r###" + __0000 + __1111 + __2222 + "###); + } + + #[test] + fn simple_ascii_one_line() { + let input = "123 456 789 abc def ghi jkl mno pqr stu vwx yz"; + let result = wrap(input, 10, "__", "", ""); + assert_snapshot!(result, @r###" + __123 456 + __789 abc + __def ghi + __jkl mno + __pqr stu + __vwx yz + "###); + } + + #[test] + fn simple_ascii_trailing_space() { + let input = "123 \n\n \n 456 \n a b \n\n"; + let result = wrap(input, 10, " ", "", ""); + assert_eq!(result, " 123\n\n\n 456\n a\n b\n\n"); + } + + #[test] + fn simple_ascii_two_lines() { + let input = "123 456 789 abc def\nghi jkl mno pqr stu vwx yz\n1234 567 89 876 54321\n"; + let result = wrap(input, 10, "__", "", ""); + assert_snapshot!(result, @r###" + __123 456 + __789 abc + __def + __ghi jkl + __mno pqr + __stu vwx + __yz + __1234 567 + __89 876 + __54321 + "###); + } + + #[test] + fn simple_ascii_no_indent() { + let input = "123 456 789\n!!abc def ghi jkl mno pqr\nstu vwx yz\n\n"; + let result = wrap(input, 10, "__", "!!", ""); + assert_snapshot!(result, @r###" + __123 456 + __789 + abc def + ghi jkl + mno pqr + __stu vwx + __yz + __ + "###); + } + + #[test] + fn simple_ascii_no_wrap() { + let input = "123 456 789\n|abc def ghi jkl mno pqr\nstu vwx yz\n|W\nA B C D E F G H I\n"; + let result = wrap(input, 10, "__", "!!", "|"); + assert_snapshot!(result, @r###" + __123 456 + __789 + __abc def ghi jkl mno pqr + __stu vwx + __yz + __W + __A B C D + __E F G H + __I + "###); + } + + #[test] + fn simple_ascii_no_both() { + let input = "123 456 789\n!!|abc def ghi jkl mno pqr\nstu vwx yz\n|W\nA B C D E F G H I\n"; + let result = wrap(input, 10, "__", "!!", "|"); + assert_snapshot!(result, @r###" + __123 456 + __789 + abc def ghi jkl mno pqr + __stu vwx + __yz + __W + __A B C D + __E F G H + __I + "###); + } + + #[test] + fn simple_ascii_no_both_wrong_order() { + let input = "!!|abc def ghi jkl\n|!!ABC DEF GHI JKL + no-sanity\n"; + let result = wrap(input, 7, "__", "!!", "|"); + assert_snapshot!(result, @r###" + abc def ghi jkl + __!!ABC DEF GHI JKL + no-sanity + "###); + let wrapped = wrap( + "ab cd ef\n!NI!123\n|AB CD EF GH\n!NI!|123 456 789", + 6, + "_", + "!NI!", + "|", + ); + assert_snapshot!(wrapped, @r###" + _ab cd + _ef + 123 + _AB CD EF GH + 123 456 789 + "###); + } + + #[test] + fn simple_ascii_much_whitespace() { + let input = "123 456 789\nabc def ghi jkl mno pqr \nstu vwx yz"; + let result = wrap(input, 10, "__", "!!", "|"); + assert_snapshot!(result, @r###" + __123 + __456 + __789 + __abc + __def ghi + __jkl mno + __pqr + __stu + __vwx yz + "###); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index fa8427b..2d952fe 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,6 @@ #[cfg(not(tarpaulin_include))] pub mod bat; +pub mod helpwrap; pub mod path; pub mod process; pub mod regex_replacement; |
