summaryrefslogtreecommitdiffstats
path: root/third_party/rust/textwrap/src/refill.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--third_party/rust/textwrap/src/refill.rs352
1 files changed, 352 insertions, 0 deletions
diff --git a/third_party/rust/textwrap/src/refill.rs b/third_party/rust/textwrap/src/refill.rs
new file mode 100644
index 0000000000..1be85f04eb
--- /dev/null
+++ b/third_party/rust/textwrap/src/refill.rs
@@ -0,0 +1,352 @@
+//! 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");
+ }
+}