Skip to content

Commit d68c270

Browse files
committed
Add new unfill and refill functions
These small helper functions makes it possible to take already wrapped text and re-wrap it to a different width. Things like Markdown unordered lists, quoted emails, and comment blocks can be rewrapped with the refill function. Fixes #60. Fixes #121.
1 parent d1ad824 commit d68c270

File tree

1 file changed

+200
-0
lines changed

1 file changed

+200
-0
lines changed

src/lib.rs

+200
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050
//! ping text.
5151
//! ```
5252
//!
53+
//! See also the [`unfill`] and [`refill`] functions which allow you to
54+
//! manipulate already wrapped text.
55+
//!
5356
//! ## Wrapping Strings at Compile Time
5457
//!
5558
//! If your strings are known at compile time, please take a look at
@@ -622,6 +625,140 @@ where
622625
result
623626
}
624627

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+
625762
/// Wrap a line of text at a given width.
626763
///
627764
/// The result is a vector of lines, each line is of type [`Cow<'_,
@@ -1556,6 +1693,69 @@ mod tests {
15561693
assert_eq!(text, "foo bar \nbaz");
15571694
}
15581695

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+
15591759
#[test]
15601760
fn trait_object() {
15611761
let opt_a: Options<NoHyphenation> = Options::with_splitter(20, NoHyphenation);

0 commit comments

Comments
 (0)