• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //! Functionality for wrapping text into columns.
2 
3 use crate::core::display_width;
4 use 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     |                |"]);
wrap_columns<'a, Opt>( text: &str, columns: usize, total_width_or_options: Opt, left_gap: &str, middle_gap: &str, right_gap: &str, ) -> Vec<String> where Opt: Into<Options<'a>>,63 pub 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>
71 where
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)]
117 mod tests {
118     use super::*;
119 
120     #[test]
wrap_columns_empty_text()121     fn wrap_columns_empty_text() {
122         assert_eq!(wrap_columns("", 1, 10, "| ", "", " |"), vec!["|        |"]);
123     }
124 
125     #[test]
wrap_columns_single_column()126     fn wrap_columns_single_column() {
127         assert_eq!(
128             wrap_columns("Foo", 3, 30, "| ", " | ", " |"),
129             vec!["| Foo    |        |          |"]
130         );
131     }
132 
133     #[test]
wrap_columns_uneven_columns()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")]
wrap_columns_with_emojis()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]
wrap_columns_big_gaps()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]
wrap_columns_panic_with_zero_columns()190     fn wrap_columns_panic_with_zero_columns() {
191         wrap_columns("", 0, 10, "", "", "");
192     }
193 }
194