|
50 | 50 | //! ping text.
|
51 | 51 | //! ```
|
52 | 52 | //!
|
| 53 | +//! See also the [`unfill`] and [`refill`] functions which allow you to |
| 54 | +//! manipulate already wrapped text. |
| 55 | +//! |
53 | 56 | //! ## Wrapping Strings at Compile Time
|
54 | 57 | //!
|
55 | 58 | //! If your strings are known at compile time, please take a look at
|
@@ -622,6 +625,140 @@ where
|
622 | 625 | result
|
623 | 626 | }
|
624 | 627 |
|
| 628 | +/// Unpack a paragraph of already-wrapped text. |
| 629 | +/// |
| 630 | +/// This function attempts to recover the original text from a single |
| 631 | +/// paragraph of text produced by the [`fill`] function. This means |
| 632 | +/// that it turns |
| 633 | +/// |
| 634 | +/// ```text |
| 635 | +/// textwrap: a small |
| 636 | +/// library for |
| 637 | +/// wrapping text. |
| 638 | +/// ``` |
| 639 | +/// |
| 640 | +/// back into |
| 641 | +/// |
| 642 | +/// ```text |
| 643 | +/// textwrap: a small library for wrapping text. |
| 644 | +/// ``` |
| 645 | +/// |
| 646 | +/// In addition, it will recognize a common prefix among the lines. |
| 647 | +/// The prefix of the first line is returned in |
| 648 | +/// [`Options::initial_indent`] and the prefix (if any) of the the |
| 649 | +/// other lines is returned in [`Options::subsequent_indent`]. |
| 650 | +/// |
| 651 | +/// In addition to `' '`, the prefixes can consist of characters used |
| 652 | +/// for unordered lists (`'-'`, `'+'`, and `'*'`) and block quotes |
| 653 | +/// (`'>'`) in Markdown as well as characters often used for inline |
| 654 | +/// comments (`'#'` and `'/'`). |
| 655 | +/// |
| 656 | +/// The text must come from a single wrapped paragraph. This means |
| 657 | +/// that there can be no `"\n\n"` within the text. |
| 658 | +/// |
| 659 | +/// # Examples |
| 660 | +/// |
| 661 | +/// ``` |
| 662 | +/// use textwrap::unfill; |
| 663 | +/// |
| 664 | +/// let (text, options) = unfill("\ |
| 665 | +/// * This is an |
| 666 | +/// example of |
| 667 | +/// a list item. |
| 668 | +/// "); |
| 669 | +/// |
| 670 | +/// assert_eq!(text, "This is an example of a list item.\n"); |
| 671 | +/// assert_eq!(options.initial_indent, "* "); |
| 672 | +/// assert_eq!(options.subsequent_indent, " "); |
| 673 | +/// ``` |
| 674 | +pub fn unfill<'a>(text: &'a str) -> (String, Options<'a, HyphenSplitter>) { |
| 675 | + let trimmed = text.trim_end_matches('\n'); |
| 676 | + let prefix_chars: &[_] = &[' ', '-', '+', '*', '>', '#', '/']; |
| 677 | + |
| 678 | + let mut options = Options::new(0); |
| 679 | + for (idx, line) in trimmed.split('\n').enumerate() { |
| 680 | + options.width = std::cmp::max(options.width, core::display_width(line)); |
| 681 | + let without_prefix = line.trim_start_matches(prefix_chars); |
| 682 | + let prefix = &line[..line.len() - without_prefix.len()]; |
| 683 | + |
| 684 | + println!("line: {:?} -> prefix: {:?}", line, prefix); |
| 685 | + |
| 686 | + if idx == 0 { |
| 687 | + options.initial_indent = prefix; |
| 688 | + } else if idx == 1 { |
| 689 | + options.subsequent_indent = prefix; |
| 690 | + } else if idx > 1 { |
| 691 | + for ((idx, x), y) in prefix.char_indices().zip(options.subsequent_indent.chars()) { |
| 692 | + if x != y { |
| 693 | + options.subsequent_indent = &prefix[..idx]; |
| 694 | + break; |
| 695 | + } |
| 696 | + } |
| 697 | + if prefix.len() < options.subsequent_indent.len() { |
| 698 | + options.subsequent_indent = prefix; |
| 699 | + } |
| 700 | + } |
| 701 | + } |
| 702 | + |
| 703 | + let mut unfilled = String::with_capacity(text.len()); |
| 704 | + for (idx, line) in trimmed.split('\n').enumerate() { |
| 705 | + if idx == 0 { |
| 706 | + unfilled.push_str(&line[options.initial_indent.len()..]); |
| 707 | + } else { |
| 708 | + unfilled.push(' '); |
| 709 | + unfilled.push_str(&line[options.subsequent_indent.len()..]); |
| 710 | + } |
| 711 | + } |
| 712 | + |
| 713 | + println!("pushing trailing newlines: {:?}", &text[trimmed.len()..]); |
| 714 | + unfilled.push_str(&text[trimmed.len()..]); |
| 715 | + |
| 716 | + println!("unfilled: {:?}", unfilled); |
| 717 | + |
| 718 | + (unfilled, options) |
| 719 | +} |
| 720 | + |
| 721 | +/// Refill a paragraph of wrapped text with a new width. |
| 722 | +/// |
| 723 | +/// This function will first use the [`unfill`] function to remove |
| 724 | +/// newlines from the text. Afterwards the text is filled again using |
| 725 | +/// the [`fill`] function. |
| 726 | +/// |
| 727 | +/// The `new_width_or_options` argument specify the new width and can |
| 728 | +/// specify other options as well — except for |
| 729 | +/// [`Options::initial_indent`] and [`Options::subsequent_indent`], |
| 730 | +/// which are deduced from `filled_text`. |
| 731 | +/// |
| 732 | +/// # Examples |
| 733 | +/// |
| 734 | +/// ``` |
| 735 | +/// use textwrap::refill; |
| 736 | +/// |
| 737 | +/// let text = "\ |
| 738 | +/// > Memory safety without |
| 739 | +/// > garbage collection. |
| 740 | +/// "; |
| 741 | +/// assert_eq!(refill(text, 15), "\ |
| 742 | +/// > Memory safety |
| 743 | +/// > without |
| 744 | +/// > garbage |
| 745 | +/// > collection. |
| 746 | +/// "); |
| 747 | +pub fn refill<'a, S, Opt>(filled_text: &str, new_width_or_options: Opt) -> String |
| 748 | +where |
| 749 | + S: WordSplitter, |
| 750 | + Opt: Into<Options<'a, S>>, |
| 751 | +{ |
| 752 | + let trimmed = filled_text.trim_end_matches('\n'); |
| 753 | + let (text, options) = unfill(trimmed); |
| 754 | + let mut new_options = new_width_or_options.into(); |
| 755 | + new_options.initial_indent = options.initial_indent; |
| 756 | + new_options.subsequent_indent = options.subsequent_indent; |
| 757 | + let mut refilled = fill(&text, new_options); |
| 758 | + refilled.push_str(&filled_text[trimmed.len()..]); |
| 759 | + refilled |
| 760 | +} |
| 761 | + |
625 | 762 | /// Wrap a line of text at a given width.
|
626 | 763 | ///
|
627 | 764 | /// The result is a vector of lines, each line is of type [`Cow<'_,
|
@@ -1556,6 +1693,69 @@ mod tests {
|
1556 | 1693 | assert_eq!(text, "foo bar \nbaz");
|
1557 | 1694 | }
|
1558 | 1695 |
|
| 1696 | + #[test] |
| 1697 | + fn unfill_simple() { |
| 1698 | + let (text, options) = unfill("foo\nbar"); |
| 1699 | + assert_eq!(text, "foo bar"); |
| 1700 | + assert_eq!(options.width, 3); |
| 1701 | + } |
| 1702 | + |
| 1703 | + #[test] |
| 1704 | + fn unfill_trailing_newlines() { |
| 1705 | + let (text, options) = unfill("foo\nbar\n\n\n"); |
| 1706 | + assert_eq!(text, "foo bar\n\n\n"); |
| 1707 | + assert_eq!(options.width, 3); |
| 1708 | + } |
| 1709 | + |
| 1710 | + #[test] |
| 1711 | + fn unfill_initial_indent() { |
| 1712 | + let (text, options) = unfill(" foo\nbar\nbaz"); |
| 1713 | + assert_eq!(text, "foo bar baz"); |
| 1714 | + assert_eq!(options.width, 5); |
| 1715 | + assert_eq!(options.initial_indent, " "); |
| 1716 | + } |
| 1717 | + |
| 1718 | + #[test] |
| 1719 | + fn unfill_differing_indents() { |
| 1720 | + let (text, options) = unfill(" foo\n bar\n baz"); |
| 1721 | + assert_eq!(text, "foo bar baz"); |
| 1722 | + assert_eq!(options.width, 7); |
| 1723 | + assert_eq!(options.initial_indent, " "); |
| 1724 | + assert_eq!(options.subsequent_indent, " "); |
| 1725 | + } |
| 1726 | + |
| 1727 | + #[test] |
| 1728 | + fn unfill_list_item() { |
| 1729 | + let (text, options) = unfill("* foo\n bar\n baz"); |
| 1730 | + assert_eq!(text, "foo bar baz"); |
| 1731 | + assert_eq!(options.width, 5); |
| 1732 | + assert_eq!(options.initial_indent, "* "); |
| 1733 | + assert_eq!(options.subsequent_indent, " "); |
| 1734 | + } |
| 1735 | + |
| 1736 | + #[test] |
| 1737 | + fn unfill_multiple_char_prefix() { |
| 1738 | + let (text, options) = unfill(" // foo bar\n // baz\n // quux"); |
| 1739 | + assert_eq!(text, "foo bar baz quux"); |
| 1740 | + assert_eq!(options.width, 14); |
| 1741 | + assert_eq!(options.initial_indent, " // "); |
| 1742 | + assert_eq!(options.subsequent_indent, " // "); |
| 1743 | + } |
| 1744 | + |
| 1745 | + #[test] |
| 1746 | + fn unfill_block_quote() { |
| 1747 | + let (text, options) = unfill("> foo\n> bar\n> baz"); |
| 1748 | + assert_eq!(text, "foo bar baz"); |
| 1749 | + assert_eq!(options.width, 5); |
| 1750 | + assert_eq!(options.initial_indent, "> "); |
| 1751 | + assert_eq!(options.subsequent_indent, "> "); |
| 1752 | + } |
| 1753 | + |
| 1754 | + #[test] |
| 1755 | + fn unfill_whitespace() { |
| 1756 | + assert_eq!(unfill("foo bar").0, "foo bar"); |
| 1757 | + } |
| 1758 | + |
1559 | 1759 | #[test]
|
1560 | 1760 | fn trait_object() {
|
1561 | 1761 | let opt_a: Options<NoHyphenation> = Options::with_splitter(20, NoHyphenation);
|
|
0 commit comments