| //! Functionality for unfilling and refilling text. |
| |
| use crate::core::display_width; |
| use crate::line_ending::NonEmptyLines; |
| use crate::{fill, LineEnding, Options}; |
| |
| /// Unpack a paragraph of already-wrapped text. |
| /// |
| /// This function attempts to recover the original text from a single |
| /// paragraph of wrapped text, such as what [`fill()`] would produce. |
| /// This means that it turns |
| /// |
| /// ```text |
| /// textwrap: a small |
| /// library for |
| /// wrapping text. |
| /// ``` |
| /// |
| /// back into |
| /// |
| /// ```text |
| /// textwrap: a small library for wrapping text. |
| /// ``` |
| /// |
| /// In addition, it will recognize a common prefix and a common line |
| /// ending among the lines. |
| /// |
| /// The prefix of the first line is returned in |
| /// [`Options::initial_indent`] and the prefix (if any) of the the |
| /// other lines is returned in [`Options::subsequent_indent`]. |
| /// |
| /// Line ending is returned in [`Options::line_ending`]. If line ending |
| /// can not be confidently detected (mixed or no line endings in the |
| /// input), [`LineEnding::LF`] will be returned. |
| /// |
| /// In addition to `' '`, the prefixes can consist of characters used |
| /// for unordered lists (`'-'`, `'+'`, and `'*'`) and block quotes |
| /// (`'>'`) in Markdown as well as characters often used for inline |
| /// comments (`'#'` and `'/'`). |
| /// |
| /// The text must come from a single wrapped paragraph. This means |
| /// that there can be no empty lines (`"\n\n"` or `"\r\n\r\n"`) within |
| /// the text. It is unspecified what happens if `unfill` is called on |
| /// more than one paragraph of text. |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// use textwrap::{LineEnding, unfill}; |
| /// |
| /// let (text, options) = unfill("\ |
| /// * This is an |
| /// example of |
| /// a list item. |
| /// "); |
| /// |
| /// assert_eq!(text, "This is an example of a list item.\n"); |
| /// assert_eq!(options.initial_indent, "* "); |
| /// assert_eq!(options.subsequent_indent, " "); |
| /// assert_eq!(options.line_ending, LineEnding::LF); |
| /// ``` |
| pub fn unfill(text: &str) -> (String, Options<'_>) { |
| let prefix_chars: &[_] = &[' ', '-', '+', '*', '>', '#', '/']; |
| |
| let mut options = Options::new(0); |
| for (idx, line) in text.lines().enumerate() { |
| options.width = std::cmp::max(options.width, display_width(line)); |
| let without_prefix = line.trim_start_matches(prefix_chars); |
| let prefix = &line[..line.len() - without_prefix.len()]; |
| |
| if idx == 0 { |
| options.initial_indent = prefix; |
| } else if idx == 1 { |
| options.subsequent_indent = prefix; |
| } else if idx > 1 { |
| for ((idx, x), y) in prefix.char_indices().zip(options.subsequent_indent.chars()) { |
| if x != y { |
| options.subsequent_indent = &prefix[..idx]; |
| break; |
| } |
| } |
| if prefix.len() < options.subsequent_indent.len() { |
| options.subsequent_indent = prefix; |
| } |
| } |
| } |
| |
| let mut unfilled = String::with_capacity(text.len()); |
| let mut detected_line_ending = None; |
| |
| for (idx, (line, ending)) in NonEmptyLines(text).enumerate() { |
| if idx == 0 { |
| unfilled.push_str(&line[options.initial_indent.len()..]); |
| } else { |
| unfilled.push(' '); |
| unfilled.push_str(&line[options.subsequent_indent.len()..]); |
| } |
| match (detected_line_ending, ending) { |
| (None, Some(_)) => detected_line_ending = ending, |
| (Some(LineEnding::CRLF), Some(LineEnding::LF)) => detected_line_ending = ending, |
| _ => (), |
| } |
| } |
| |
| // Add back a line ending if `text` ends with the one we detect. |
| if let Some(line_ending) = detected_line_ending { |
| if text.ends_with(line_ending.as_str()) { |
| unfilled.push_str(line_ending.as_str()); |
| } |
| } |
| |
| options.line_ending = detected_line_ending.unwrap_or(LineEnding::LF); |
| (unfilled, options) |
| } |
| |
| /// Refill a paragraph of wrapped text with a new width. |
| /// |
| /// This function will first use [`unfill()`] to remove newlines from |
| /// the text. Afterwards the text is filled again using [`fill()`]. |
| /// |
| /// The `new_width_or_options` argument specify the new width and can |
| /// specify other options as well — except for |
| /// [`Options::initial_indent`] and [`Options::subsequent_indent`], |
| /// which are deduced from `filled_text`. |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// use textwrap::refill; |
| /// |
| /// // Some loosely wrapped text. The "> " prefix is recognized automatically. |
| /// let text = "\ |
| /// > Memory |
| /// > safety without garbage |
| /// > collection. |
| /// "; |
| /// |
| /// assert_eq!(refill(text, 20), "\ |
| /// > Memory safety |
| /// > without garbage |
| /// > collection. |
| /// "); |
| /// |
| /// assert_eq!(refill(text, 40), "\ |
| /// > Memory safety without garbage |
| /// > collection. |
| /// "); |
| /// |
| /// assert_eq!(refill(text, 60), "\ |
| /// > Memory safety without garbage collection. |
| /// "); |
| /// ``` |
| /// |
| /// You can also reshape bullet points: |
| /// |
| /// ``` |
| /// use textwrap::refill; |
| /// |
| /// let text = "\ |
| /// - This is my |
| /// list item. |
| /// "; |
| /// |
| /// assert_eq!(refill(text, 20), "\ |
| /// - This is my list |
| /// item. |
| /// "); |
| /// ``` |
| pub fn refill<'a, Opt>(filled_text: &str, new_width_or_options: Opt) -> String |
| where |
| Opt: Into<Options<'a>>, |
| { |
| let mut new_options = new_width_or_options.into(); |
| let (text, options) = unfill(filled_text); |
| // The original line ending is kept by `unfill`. |
| let stripped = text.strip_suffix(options.line_ending.as_str()); |
| let new_line_ending = new_options.line_ending.as_str(); |
| |
| new_options.initial_indent = options.initial_indent; |
| new_options.subsequent_indent = options.subsequent_indent; |
| let mut refilled = fill(stripped.unwrap_or(&text), new_options); |
| |
| // Add back right line ending if we stripped one off above. |
| if stripped.is_some() { |
| refilled.push_str(new_line_ending); |
| } |
| refilled |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn unfill_simple() { |
| let (text, options) = unfill("foo\nbar"); |
| assert_eq!(text, "foo bar"); |
| assert_eq!(options.width, 3); |
| assert_eq!(options.line_ending, LineEnding::LF); |
| } |
| |
| #[test] |
| fn unfill_no_new_line() { |
| let (text, options) = unfill("foo bar"); |
| assert_eq!(text, "foo bar"); |
| assert_eq!(options.width, 7); |
| assert_eq!(options.line_ending, LineEnding::LF); |
| } |
| |
| #[test] |
| fn unfill_simple_crlf() { |
| let (text, options) = unfill("foo\r\nbar"); |
| assert_eq!(text, "foo bar"); |
| assert_eq!(options.width, 3); |
| assert_eq!(options.line_ending, LineEnding::CRLF); |
| } |
| |
| #[test] |
| fn unfill_mixed_new_lines() { |
| let (text, options) = unfill("foo\r\nbar\nbaz"); |
| assert_eq!(text, "foo bar baz"); |
| assert_eq!(options.width, 3); |
| assert_eq!(options.line_ending, LineEnding::LF); |
| } |
| |
| #[test] |
| fn test_unfill_consecutive_different_prefix() { |
| let (text, options) = unfill("foo\n*\n/"); |
| assert_eq!(text, "foo * /"); |
| assert_eq!(options.width, 3); |
| assert_eq!(options.line_ending, LineEnding::LF); |
| } |
| |
| #[test] |
| fn unfill_trailing_newlines() { |
| let (text, options) = unfill("foo\nbar\n\n\n"); |
| assert_eq!(text, "foo bar\n"); |
| assert_eq!(options.width, 3); |
| } |
| |
| #[test] |
| fn unfill_mixed_trailing_newlines() { |
| let (text, options) = unfill("foo\r\nbar\n\r\n\n"); |
| assert_eq!(text, "foo bar\n"); |
| assert_eq!(options.width, 3); |
| assert_eq!(options.line_ending, LineEnding::LF); |
| } |
| |
| #[test] |
| fn unfill_trailing_crlf() { |
| let (text, options) = unfill("foo bar\r\n"); |
| assert_eq!(text, "foo bar\r\n"); |
| assert_eq!(options.width, 7); |
| assert_eq!(options.line_ending, LineEnding::CRLF); |
| } |
| |
| #[test] |
| fn unfill_initial_indent() { |
| let (text, options) = unfill(" foo\nbar\nbaz"); |
| assert_eq!(text, "foo bar baz"); |
| assert_eq!(options.width, 5); |
| assert_eq!(options.initial_indent, " "); |
| } |
| |
| #[test] |
| fn unfill_differing_indents() { |
| let (text, options) = unfill(" foo\n bar\n baz"); |
| assert_eq!(text, "foo bar baz"); |
| assert_eq!(options.width, 7); |
| assert_eq!(options.initial_indent, " "); |
| assert_eq!(options.subsequent_indent, " "); |
| } |
| |
| #[test] |
| fn unfill_list_item() { |
| let (text, options) = unfill("* foo\n bar\n baz"); |
| assert_eq!(text, "foo bar baz"); |
| assert_eq!(options.width, 5); |
| assert_eq!(options.initial_indent, "* "); |
| assert_eq!(options.subsequent_indent, " "); |
| } |
| |
| #[test] |
| fn unfill_multiple_char_prefix() { |
| let (text, options) = unfill(" // foo bar\n // baz\n // quux"); |
| assert_eq!(text, "foo bar baz quux"); |
| assert_eq!(options.width, 14); |
| assert_eq!(options.initial_indent, " // "); |
| assert_eq!(options.subsequent_indent, " // "); |
| } |
| |
| #[test] |
| fn unfill_block_quote() { |
| let (text, options) = unfill("> foo\n> bar\n> baz"); |
| assert_eq!(text, "foo bar baz"); |
| assert_eq!(options.width, 5); |
| assert_eq!(options.initial_indent, "> "); |
| assert_eq!(options.subsequent_indent, "> "); |
| } |
| |
| #[test] |
| fn unfill_only_prefixes_issue_466() { |
| // Test that we don't crash if the first line has only prefix |
| // chars *and* the second line is shorter than the first line. |
| let (text, options) = unfill("######\nfoo"); |
| assert_eq!(text, " foo"); |
| assert_eq!(options.width, 6); |
| assert_eq!(options.initial_indent, "######"); |
| assert_eq!(options.subsequent_indent, ""); |
| } |
| |
| #[test] |
| fn unfill_trailing_newlines_issue_466() { |
| // Test that we don't crash on a '\r' following a string of |
| // '\n'. The problem was that we removed both kinds of |
| // characters in one code path, but not in the other. |
| let (text, options) = unfill("foo\n##\n\n\r"); |
| // The \n\n changes subsequent_indent to "". |
| assert_eq!(text, "foo ## \r"); |
| assert_eq!(options.width, 3); |
| assert_eq!(options.initial_indent, ""); |
| assert_eq!(options.subsequent_indent, ""); |
| } |
| |
| #[test] |
| fn unfill_whitespace() { |
| assert_eq!(unfill("foo bar").0, "foo bar"); |
| } |
| |
| #[test] |
| fn refill_convert_lf_to_crlf() { |
| let options = Options::new(5).line_ending(LineEnding::CRLF); |
| assert_eq!(refill("foo\nbar\n", options), "foo\r\nbar\r\n",); |
| } |
| |
| #[test] |
| fn refill_convert_crlf_to_lf() { |
| let options = Options::new(5).line_ending(LineEnding::LF); |
| assert_eq!(refill("foo\r\nbar\r\n", options), "foo\nbar\n",); |
| } |
| |
| #[test] |
| fn refill_convert_mixed_newlines() { |
| let options = Options::new(5).line_ending(LineEnding::CRLF); |
| assert_eq!(refill("foo\r\nbar\n", options), "foo\r\nbar\r\n",); |
| } |
| |
| #[test] |
| fn refill_defaults_to_lf() { |
| assert_eq!(refill("foo bar baz", 5), "foo\nbar\nbaz"); |
| } |
| } |