1use super::text::{Token, WidthMode, tokenize};
20
21#[inline]
22fn bs(b: &[u8]) -> &str {
23 unsafe { std::str::from_utf8_unchecked(b) }
25}
26
27pub const DEFAULT_BREAKPOINTS: &str = "-,.;: ";
29
30pub fn hardwrap(s: &str, limit: usize, preserve_space: bool) -> String {
34 hardwrap_mode(s, limit, preserve_space, WidthMode::default(), false)
35}
36
37pub fn hardwrap_mode(
41 s: &str,
42 limit: usize,
43 preserve_space: bool,
44 mode: WidthMode,
45 eaw_wide: bool,
46) -> String {
47 if limit == 0 {
48 return s.to_string();
49 }
50 let mut out = String::with_capacity(s.len());
51 let mut col = 0usize;
52 for tok in tokenize(s.as_bytes(), mode, eaw_wide) {
53 match tok {
54 Token::Escape(e) => out.push_str(bs(e)),
55 Token::Control(b'\n') => {
56 out.push('\n');
57 col = 0;
58 }
59 Token::Control(b) => out.push(b as char),
60 Token::Text { text, width } => {
61 let w = width as usize;
62 if w == 0 {
63 out.push_str(bs(text));
64 continue;
65 }
66 if col + w > limit {
67 out.push('\n');
68 col = 0;
69 if !preserve_space && text == b" " {
70 continue;
71 }
72 }
73 out.push_str(bs(text));
74 col += w;
75 }
76 }
77 }
78 out
79}
80
81pub fn wordwrap(s: &str, limit: usize, breakpoints: &str) -> String {
85 wordwrap_mode(s, limit, breakpoints, WidthMode::default(), false)
86}
87
88pub fn wordwrap_mode(
92 s: &str,
93 limit: usize,
94 breakpoints: &str,
95 mode: WidthMode,
96 eaw_wide: bool,
97) -> String {
98 if limit == 0 {
99 return s.to_string();
100 }
101 let bp: Vec<char> = breakpoints.chars().collect();
102
103 let mut out = String::with_capacity(s.len());
108 let mut line = String::new();
109 let mut line_w = 0usize;
110 let mut word = String::new();
111 let mut word_w = 0usize;
112 let mut space = String::new();
113 let mut space_w = 0usize;
114
115 let flush_word_to_line = |line: &mut String,
116 line_w: &mut usize,
117 word: &mut String,
118 word_w: &mut usize,
119 space: &mut String,
120 space_w: &mut usize| {
121 if !word.is_empty() {
122 line.push_str(space);
125 line.push_str(word);
126 *line_w += *space_w + *word_w;
127 space.clear();
128 *space_w = 0;
129 word.clear();
130 *word_w = 0;
131 }
132 };
133
134 for tok in tokenize(s.as_bytes(), mode, eaw_wide) {
135 match tok {
136 Token::Escape(e) => {
137 if !word.is_empty() {
139 word.push_str(bs(e));
140 } else if !space.is_empty() {
141 space.push_str(bs(e));
142 } else {
143 line.push_str(bs(e));
144 }
145 }
146 Token::Control(b'\n') => {
147 flush_word_to_line(
149 &mut line,
150 &mut line_w,
151 &mut word,
152 &mut word_w,
153 &mut space,
154 &mut space_w,
155 );
156 out.push_str(&line);
157 out.push('\n');
158 line.clear();
159 line_w = 0;
160 space.clear();
161 space_w = 0;
162 }
163 Token::Control(b) => {
164 word.push(b as char);
166 }
167 Token::Text { text, width } => {
168 let w = width as usize;
169 let is_break = bs(text).chars().all(|c| bp.contains(&c));
170 let is_space = text == b" " || text == b"\t";
171
172 if is_space {
173 if !word.is_empty() {
176 if line_w > 0 && line_w + space_w + word_w > limit {
178 out.push_str(&line);
179 out.push('\n');
180 line.clear();
181 line_w = 0;
182 space.clear();
183 space_w = 0;
184 }
185 flush_word_to_line(
186 &mut line,
187 &mut line_w,
188 &mut word,
189 &mut word_w,
190 &mut space,
191 &mut space_w,
192 );
193 }
194 space.push_str(bs(text));
195 space_w += w;
196 } else if is_break {
197 word.push_str(bs(text));
200 word_w += w;
201 if line_w > 0 && line_w + space_w + word_w > limit {
203 out.push_str(&line);
204 out.push('\n');
205 line.clear();
206 line_w = 0;
207 space.clear();
208 space_w = 0;
209 }
210 flush_word_to_line(
211 &mut line,
212 &mut line_w,
213 &mut word,
214 &mut word_w,
215 &mut space,
216 &mut space_w,
217 );
218 } else {
219 word.push_str(bs(text));
220 word_w += w;
221 }
222 }
223 }
224 }
225
226 if !word.is_empty() {
228 if line_w > 0 && line_w + space_w + word_w > limit {
229 out.push_str(&line);
230 out.push('\n');
231 line.clear();
232 line_w = 0;
233 space.clear();
234 space_w = 0;
235 }
236 flush_word_to_line(
237 &mut line,
238 &mut line_w,
239 &mut word,
240 &mut word_w,
241 &mut space,
242 &mut space_w,
243 );
244 } else if !space.is_empty() {
245 line.push_str(&space);
247 }
248 out.push_str(&line);
249 out
250}
251
252pub fn wrap(s: &str, limit: usize, breakpoints: &str) -> String {
256 wrap_mode(s, limit, breakpoints, WidthMode::default(), false)
257}
258
259pub fn wrap_mode(
263 s: &str,
264 limit: usize,
265 breakpoints: &str,
266 mode: WidthMode,
267 eaw_wide: bool,
268) -> String {
269 if limit == 0 {
270 return s.to_string();
271 }
272 let wrapped = wordwrap_mode(s, limit, breakpoints, mode, eaw_wide);
275 let mut out = String::with_capacity(wrapped.len());
276 for (i, line) in wrapped.split('\n').enumerate() {
277 if i > 0 {
278 out.push('\n');
279 }
280 out.push_str(&hardwrap_mode(line, limit, false, mode, eaw_wide));
281 }
282 out
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn hardwrap_basic() {
291 assert_eq!(hardwrap("hello world", 5, true), "hello\n worl\nd");
292 }
293
294 #[test]
295 fn hardwrap_no_preserve_space() {
296 assert_eq!(hardwrap("hello world", 5, false), "hello\nworld");
297 }
298
299 #[test]
300 fn hardwrap_preserves_ansi() {
301 let s = "\x1b[31mhello world\x1b[m";
302 let got = hardwrap(s, 5, false);
303 assert_eq!(got, "\x1b[31mhello\nworld\x1b[m");
304 }
305
306 #[test]
307 fn hardwrap_explicit_newline() {
308 assert_eq!(hardwrap("ab\ncd", 5, true), "ab\ncd");
309 }
310
311 #[test]
312 fn wordwrap_basic() {
313 assert_eq!(wordwrap("hello world", 5, " "), "hello\nworld");
314 }
315
316 #[test]
317 fn wordwrap_long_word_not_broken() {
318 assert_eq!(wordwrap("abcdefghij", 5, " "), "abcdefghij");
320 }
321
322 #[test]
323 fn wordwrap_explicit_newline() {
324 assert_eq!(wordwrap("a b\nc d", 10, " "), "a b\nc d");
325 }
326
327 #[test]
328 fn wordwrap_with_hyphen_break() {
329 let got = wordwrap("foo-bar-baz", 4, DEFAULT_BREAKPOINTS);
330 assert_eq!(got, "foo-\nbar-\nbaz");
331 }
332
333 #[test]
334 fn wrap_breaks_long_words() {
335 assert_eq!(wrap("abcdefghij", 5, " "), "abcde\nfghij");
336 }
337
338 #[test]
339 fn wrap_mixed() {
340 assert_eq!(
341 wrap("hello superlongword end", 5, " "),
342 "hello\nsuper\nlongw\nord\nend"
343 );
344 }
345
346 #[test]
347 fn wordwrap_zero_limit_returns_input() {
348 assert_eq!(wordwrap("anything", 0, " "), "anything");
349 }
350}