kumo_wrap/
lib.rs

1use bstr::{BStr, ByteVec};
2
3pub const SOFT_WIDTH: usize = 75;
4pub const HARD_WIDTH: usize = 900;
5
6pub fn wrap(value: &str) -> String {
7    String::from_utf8(wrap_impl(value, SOFT_WIDTH, HARD_WIDTH)).expect("utf8-in, utf8-out")
8}
9
10pub fn wrap_bytes(value: impl AsRef<BStr>) -> Vec<u8> {
11    wrap_impl(value, SOFT_WIDTH, HARD_WIDTH)
12}
13
14/// We can't use textwrap::fill here because it will prefer to break
15/// a line rather than finding stuff that fits.  We use a simple
16/// algorithm that tries to fill up to the desired width, allowing
17/// for overflow if there is a word that is too long to fit in
18/// the header, but breaking after a hard limit threshold.
19pub fn wrap_impl(value: impl AsRef<BStr>, soft_width: usize, hard_width: usize) -> Vec<u8> {
20    let value: &BStr = value.as_ref();
21    let mut result: Vec<u8> = vec![];
22    let mut line: Vec<u8> = vec![];
23
24    for word in value.split(|&b| b.is_ascii_whitespace()) {
25        if word.len() == 0 {
26            continue;
27        }
28        if line.len() + word.len() < soft_width {
29            if !line.is_empty() {
30                line.push(b' ');
31            }
32            line.push_str(word);
33            continue;
34        }
35
36        // Need to wrap.
37
38        // Accumulate line so far, if any
39        if !line.is_empty() {
40            if !result.is_empty() {
41                // There's an existing line, start a new one, indented
42                result.push(b'\t');
43            }
44            result.push_str(&line);
45            result.push_str("\r\n");
46            line.clear();
47        }
48
49        // build out a line from the characters of this one
50        if word.len() <= hard_width {
51            line.push_str(word);
52        } else {
53            for &c in word.iter() {
54                line.push(c);
55                if line.len() >= hard_width {
56                    if !result.is_empty() {
57                        result.push(b'\t');
58                    }
59                    result.push_str(&line);
60                    result.push_str("\r\n");
61                    line.clear();
62                    continue;
63                }
64            }
65        }
66    }
67
68    if !line.is_empty() {
69        if !result.is_empty() {
70            result.push(b'\t');
71        }
72        result.push_str(&line);
73    }
74
75    result
76}
77
78#[cfg(test)]
79mod test {
80    use super::*;
81
82    #[test]
83    fn wrapping() {
84        for (input, expect) in [
85            ("foo", "foo"),
86            ("hi there", "hi there"),
87            ("hello world", "hello\r\n\tworld"),
88            ("hello world ", "hello\r\n\tworld"),
89            (
90                "hello world foo bar baz woot woot",
91                "hello\r\n\tworld foo\r\n\tbar baz\r\n\twoot woot",
92            ),
93            (
94                "hi there breakmepleaseIamtoolong",
95                "hi there\r\n\tbreakmepleaseIa\r\n\tmtoolong",
96            ),
97        ] {
98            let wrapped = wrap_impl(input, 10, 15);
99            k9::assert_equal!(
100                wrapped,
101                expect.as_bytes(),
102                "input: '{input}' should produce '{expect}'"
103            );
104        }
105    }
106}