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(&current_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(&current_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}