1use unicode_segmentation::UnicodeSegmentation;
2use unicode_width::UnicodeWidthStr;
3
4const ELLIPSIS: &str = "…";
5const TAB: &str = " ";
6
7pub fn clean(text: &str) -> String {
8 let mut out = String::with_capacity(text.len());
9 for c in text.chars() {
10 match c {
11 '\t' => out.push_str(TAB),
12 '\n' | '\r' => out.push(' '),
13 c if c.is_control() => {}
14 c => out.push(c),
15 }
16 }
17 out
18}
19
20pub fn cells(text: &str) -> usize {
21 text.width()
22}
23
24pub fn split(text: &str, width: usize) -> (&str, &str) {
25 let mut used = 0;
26 let mut end = 0;
27 for (index, glyph) in text.grapheme_indices(true) {
28 let wide = glyph.width();
29 if used + wide > width {
30 break;
31 }
32 used += wide;
33 end = index + glyph.len();
34 }
35 text.split_at(end)
36}
37
38pub fn clip(text: &str, width: usize) -> String {
39 if cells(text) <= width {
40 return text.to_string();
41 }
42 if width == 0 {
43 return String::new();
44 }
45 let (kept, _) = split(text, width - 1);
46 format!("{kept}{ELLIPSIS}")
47}
48
49pub fn fit(text: &str, width: usize) -> String {
50 let clipped = clip(text, width);
51 let pad = width.saturating_sub(cells(&clipped));
52 format!("{clipped}{}", " ".repeat(pad))
53}
54
55pub fn wrap(text: &str, width: usize) -> Vec<String> {
56 if width == 0 {
57 return Vec::new();
58 }
59 let mut lines = Vec::new();
60 for paragraph in text.split('\n') {
61 let paragraph = clean(paragraph);
62 let mut rest = paragraph.trim_end();
63 if rest.is_empty() {
64 lines.push(String::new());
65 continue;
66 }
67 while !rest.is_empty() {
68 if cells(rest) <= width {
69 lines.push(rest.to_string());
70 break;
71 }
72 let (head, _) = split(rest, width);
73 let cut = match head.rfind(' ') {
74 Some(space) if space > 0 && rest[head.len()..].starts_with(' ') => head.len(),
75 Some(space) if space > 0 => space,
76 _ if head.is_empty() => rest
77 .grapheme_indices(true)
78 .nth(1)
79 .map_or(rest.len(), |(index, _)| index),
80 _ => head.len(),
81 };
82 lines.push(rest[..cut].trim_end().to_string());
83 rest = rest[cut..].trim_start();
84 }
85 }
86 lines
87}
88
89pub fn hard_wrap(text: &str, width: usize) -> Vec<String> {
90 if width == 0 {
91 return Vec::new();
92 }
93 let mut lines = Vec::new();
94 for paragraph in text.split('\n') {
95 let paragraph = clean(paragraph);
96 let mut rest = paragraph.trim_end();
97 if rest.is_empty() {
98 lines.push(String::new());
99 continue;
100 }
101 while !rest.is_empty() {
102 let (head, tail) = split(rest, width);
103 let (head, tail) = if head.is_empty() {
104 let cut = rest
105 .grapheme_indices(true)
106 .nth(1)
107 .map_or(rest.len(), |(index, _)| index);
108 rest.split_at(cut)
109 } else {
110 (head, tail)
111 };
112 lines.push(head.to_string());
113 rest = tail;
114 }
115 }
116 lines
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn widths_are_counted_in_cells_and_wide_glyphs_are_never_cut() {
125 assert_eq!(cells("ăîșț"), 4);
126 assert_eq!(cells("日本"), 4);
127 assert_eq!(clip("日本語のテキスト", 5), "日本…");
128 assert_eq!(fit("ab", 4), "ab ");
129 assert_eq!(clip("short", 9), "short");
130 assert_eq!(wrap("the quick brown fox", 9), ["the quick", "brown fox"]);
131 assert_eq!(wrap("abcdefghij", 4), ["abcd", "efgh", "ij"]);
132 assert_eq!(wrap("日本語", 1), ["日", "本", "語"]);
133 assert_eq!(hard_wrap("the quick brown", 6), ["the qu", "ick br", "own"]);
134 assert_eq!(hard_wrap("a日本\n\nb", 2), ["a", "日", "本", "", "b"]);
135 }
136}