textwrap/
columns.rs

1//! Functionality for wrapping text into columns.
2
3use crate::core::display_width;
4use crate::{wrap, Options};
5
6/// Wrap text into columns with a given total width.
7///
8/// The `left_gap`, `middle_gap` and `right_gap` arguments specify the
9/// strings to insert before, between, and after the columns. The
10/// total width of all columns and all gaps is specified using the
11/// `total_width_or_options` argument. This argument can simply be an
12/// integer if you want to use default settings when wrapping, or it
13/// can be a [`Options`] value if you want to customize the wrapping.
14///
15/// If the columns are narrow, it is recommended to set
16/// [`Options::break_words`] to `true` to prevent words from
17/// protruding into the margins.
18///
19/// The per-column width is computed like this:
20///
21/// ```
22/// # let (left_gap, middle_gap, right_gap) = ("", "", "");
23/// # let columns = 2;
24/// # let options = textwrap::Options::new(80);
25/// let inner_width = options.width
26///     - textwrap::core::display_width(left_gap)
27///     - textwrap::core::display_width(right_gap)
28///     - textwrap::core::display_width(middle_gap) * (columns - 1);
29/// let column_width = inner_width / columns;
30/// ```
31///
32/// The `text` is wrapped using [`wrap()`] and the given `options`
33/// argument, but the width is overwritten to the computed
34/// `column_width`.
35///
36/// # Panics
37///
38/// Panics if `columns` is zero.
39///
40/// # Examples
41///
42/// ```
43/// use textwrap::wrap_columns;
44///
45/// let text = "\
46/// This is an example text, which is wrapped into three columns. \
47/// Notice how the final column can be shorter than the others.";
48///
49/// #[cfg(feature = "smawk")]
50/// assert_eq!(wrap_columns(text, 3, 50, "| ", " | ", " |"),
51///            vec!["| This is       | into three    | column can be  |",
52///                 "| an example    | columns.      | shorter than   |",
53///                 "| text, which   | Notice how    | the others.    |",
54///                 "| is wrapped    | the final     |                |"]);
55///
56/// // Without the `smawk` feature, the middle column is a little more uneven:
57/// #[cfg(not(feature = "smawk"))]
58/// assert_eq!(wrap_columns(text, 3, 50, "| ", " | ", " |"),
59///            vec!["| This is an    | three         | column can be  |",
60///                 "| example text, | columns.      | shorter than   |",
61///                 "| which is      | Notice how    | the others.    |",
62///                 "| wrapped into  | the final     |                |"]);
63pub fn wrap_columns<'a, Opt>(
64    text: &str,
65    columns: usize,
66    total_width_or_options: Opt,
67    left_gap: &str,
68    middle_gap: &str,
69    right_gap: &str,
70) -> Vec<String>
71where
72    Opt: Into<Options<'a>>,
73{
74    assert!(columns > 0);
75
76    let mut options: Options = total_width_or_options.into();
77
78    let inner_width = options
79        .width
80        .saturating_sub(display_width(left_gap))
81        .saturating_sub(display_width(right_gap))
82        .saturating_sub(display_width(middle_gap) * (columns - 1));
83
84    let column_width = std::cmp::max(inner_width / columns, 1);
85    options.width = column_width;
86    let last_column_padding = " ".repeat(inner_width % column_width);
87    let wrapped_lines = wrap(text, options);
88    let lines_per_column =
89        wrapped_lines.len() / columns + usize::from(wrapped_lines.len() % columns > 0);
90    let mut lines = Vec::new();
91    for line_no in 0..lines_per_column {
92        let mut line = String::from(left_gap);
93        for column_no in 0..columns {
94            match wrapped_lines.get(line_no + column_no * lines_per_column) {
95                Some(column_line) => {
96                    line.push_str(column_line);
97                    line.push_str(&" ".repeat(column_width - display_width(column_line)));
98                }
99                None => {
100                    line.push_str(&" ".repeat(column_width));
101                }
102            }
103            if column_no == columns - 1 {
104                line.push_str(&last_column_padding);
105            } else {
106                line.push_str(middle_gap);
107            }
108        }
109        line.push_str(right_gap);
110        lines.push(line);
111    }
112
113    lines
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn wrap_columns_empty_text() {
122        assert_eq!(wrap_columns("", 1, 10, "| ", "", " |"), vec!["|        |"]);
123    }
124
125    #[test]
126    fn wrap_columns_single_column() {
127        assert_eq!(
128            wrap_columns("Foo", 3, 30, "| ", " | ", " |"),
129            vec!["| Foo    |        |          |"]
130        );
131    }
132
133    #[test]
134    fn wrap_columns_uneven_columns() {
135        // The gaps take up a total of 5 columns, so the columns are
136        // (21 - 5)/4 = 4 columns wide:
137        assert_eq!(
138            wrap_columns("Foo Bar Baz Quux", 4, 21, "|", "|", "|"),
139            vec!["|Foo |Bar |Baz |Quux|"]
140        );
141        // As the total width increases, the last column absorbs the
142        // excess width:
143        assert_eq!(
144            wrap_columns("Foo Bar Baz Quux", 4, 24, "|", "|", "|"),
145            vec!["|Foo |Bar |Baz |Quux   |"]
146        );
147        // Finally, when the width is 25, the columns can be resized
148        // to a width of (25 - 5)/4 = 5 columns:
149        assert_eq!(
150            wrap_columns("Foo Bar Baz Quux", 4, 25, "|", "|", "|"),
151            vec!["|Foo  |Bar  |Baz  |Quux |"]
152        );
153    }
154
155    #[test]
156    #[cfg(feature = "unicode-width")]
157    fn wrap_columns_with_emojis() {
158        assert_eq!(
159            wrap_columns(
160                "Words and a few emojis 😍 wrapped in ⓶ columns",
161                2,
162                30,
163                "✨ ",
164                " ⚽ ",
165                " 👀"
166            ),
167            vec![
168                "✨ Words      ⚽ wrapped in 👀",
169                "✨ and a few  ⚽ ⓶ columns  👀",
170                "✨ emojis 😍  ⚽            👀"
171            ]
172        );
173    }
174
175    #[test]
176    fn wrap_columns_big_gaps() {
177        // The column width shrinks to 1 because the gaps take up all
178        // the space.
179        assert_eq!(
180            wrap_columns("xyz", 2, 10, "----> ", " !!! ", " <----"),
181            vec![
182                "----> x !!! z <----", //
183                "----> y !!!   <----"
184            ]
185        );
186    }
187
188    #[test]
189    #[should_panic]
190    fn wrap_columns_panic_with_zero_columns() {
191        wrap_columns("", 0, 10, "", "", "");
192    }
193}