comfy_table/utils/formatting/content_split/mod.rs
1use crate::utils::ColumnDisplayInfo;
2
3#[cfg(feature = "custom_styling")]
4mod custom_styling;
5#[cfg(not(feature = "custom_styling"))]
6mod normal;
7
8#[cfg(feature = "custom_styling")]
9pub use custom_styling::*;
10#[cfg(not(feature = "custom_styling"))]
11pub use normal::*;
12
13/// Split a line if it's longer than the allowed columns (width - padding).
14///
15/// This function tries to do this in a smart way, by splitting the content
16/// with a given delimiter at the very beginning.
17/// These "elements" then get added one-by-one to the lines, until a line is full.
18/// As soon as the line is full, we add it to the result set and start a new line.
19///
20/// This is repeated until there are no more "elements".
21///
22/// Mid-element splits only occurs if an element doesn't fit in a single line by itself.
23pub fn split_line(line: &str, info: &ColumnDisplayInfo, delimiter: char) -> Vec<String> {
24 let mut lines = Vec::new();
25 let content_width = usize::from(info.content_width);
26
27 // Split the line by the given deliminator and turn the content into a stack.
28 // Also clone it and convert it into a Vec<String>. Otherwise, we get some burrowing problems
29 // due to early drops of borrowed values that need to be inserted into `Vec<&str>`
30 let mut elements = split_line_by_delimiter(line, delimiter);
31
32 // Reverse it, since we want to push/pop without reversing the text.
33 elements.reverse();
34
35 let mut current_line = String::new();
36 while let Some(next) = elements.pop() {
37 let current_length = measure_text_width(¤t_line);
38 let next_length = measure_text_width(&next);
39
40 // Some helper variables
41 // The length of the current line when combining it with the next element
42 // Add 1 for the delimiter if we are on a non-empty line.
43 let mut added_length = next_length + current_length;
44 if !current_line.is_empty() {
45 added_length += 1;
46 }
47 // The remaining width for this column. If we are on a non-empty line, subtract 1 for the delimiter.
48 let mut remaining_width = content_width - current_length;
49 if !current_line.is_empty() {
50 remaining_width = remaining_width.saturating_sub(1);
51 }
52
53 // The next element fits into the current line
54 if added_length <= content_width {
55 // Only add delimiter, if we're not on a fresh line
56 if !current_line.is_empty() {
57 current_line.push(delimiter);
58 }
59 current_line += &next;
60
61 // Already complete the current line, if there isn't space for more than two chars
62 current_line = check_if_full(&mut lines, content_width, current_line);
63 continue;
64 }
65
66 // The next element doesn't fit in the current line
67
68 // Check, if there's enough space in the current line in case we decide to split the
69 // element and only append a part of it to the current line.
70 // If there isn't enough space, we simply push the current line, put the element back
71 // on stack and start with a fresh line.
72 if !current_line.is_empty() && remaining_width <= MIN_FREE_CHARS {
73 elements.push(next);
74 lines.push(current_line);
75 current_line = String::new();
76
77 continue;
78 }
79
80 // Ok. There's still enough space to fit something in (more than MIN_FREE_CHARS characters)
81 // There are two scenarios:
82 //
83 // 1. The word is too long for a single line.
84 // In this case, we have to split the element anyway. Let's fill the remaining space on
85 // the current line with, start a new line and push the remaining part on the stack.
86 // 2. The word is short enough to fit as a whole into a line
87 // In that case we simply push the current line and start a new one with the current element
88
89 // Case 1
90 // The element is longer than the specified content_width
91 // Split the word, push the remaining string back on the stack
92 if next_length > content_width {
93 let new_line = current_line.is_empty();
94
95 // Only add delimiter, if we're not on a fresh line
96 if !new_line {
97 current_line.push(delimiter);
98 }
99
100 let (mut next, mut remaining) = split_long_word(remaining_width, &next);
101
102 // This is an ugly hack, but it's needed for now.
103 //
104 // Scenario: The current column has to have a width of 1, and we work with a new line.
105 // However, the next char is a multi-character UTF-8 symbol.
106 //
107 // Since a multi-character wide symbol doesn't fit into a 1-character column,
108 // this code would loop endlessly. (There's no legitimate way to split that character.)
109 // Hence, we have to live with the fact, that this line will look broken, as we put a
110 // two-character wide symbol into it, despite the line being formatted for 1 character.
111 if new_line && next.is_empty() {
112 let mut chars = remaining.chars();
113 next.push(chars.next().unwrap());
114 remaining = chars.collect();
115 }
116
117 current_line += &next;
118 elements.push(remaining);
119
120 // Push the finished line, and start a new one
121 lines.push(current_line);
122 current_line = String::new();
123
124 continue;
125 }
126
127 // Case 2
128 // The element fits into a single line.
129 // Push the current line and initialize the next line with the element.
130 lines.push(current_line);
131 current_line = next.to_string();
132 current_line = check_if_full(&mut lines, content_width, current_line);
133 }
134
135 if !current_line.is_empty() {
136 lines.push(current_line);
137 }
138
139 lines
140}
141
142/// This is the minimum of available characters per line.
143/// It's used to check, whether another element can be added to the current line.
144/// Otherwise, the line will simply be left as it is, and we start with a new one.
145/// Two chars seems like a reasonable approach, since this would require next element to be
146/// a single char + delimiter.
147const MIN_FREE_CHARS: usize = 2;
148
149/// Check if the current line is too long and whether we should start a new one
150/// If it's too long, we add the current line to the list of lines and return a new [String].
151/// Otherwise, we simply return the current line and basically don't do anything.
152fn check_if_full(lines: &mut Vec<String>, content_width: usize, current_line: String) -> String {
153 // Already complete the current line, if there isn't space for more than two chars
154 if measure_text_width(¤t_line) > content_width.saturating_sub(MIN_FREE_CHARS) {
155 lines.push(current_line);
156 return String::new();
157 }
158
159 current_line
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use unicode_width::UnicodeWidthStr;
166
167 #[test]
168 fn test_split_long_word() {
169 let emoji = "🙂↕️"; // U+1F642 U+200D U+2195 U+FE0F head shaking vertically
170 assert_eq!(emoji.len(), 13);
171 assert_eq!(emoji.chars().count(), 4);
172 assert_eq!(emoji.width(), 2);
173
174 let (word, remaining) = split_long_word(emoji.width(), emoji);
175
176 assert_eq!(word, "\u{1F642}\u{200D}\u{2195}\u{FE0F}");
177 assert_eq!(word.len(), 13);
178 assert_eq!(word.chars().count(), 4);
179 assert_eq!(word.width(), 2);
180
181 assert!(remaining.is_empty());
182 }
183}