Skip to main content

str_utils/
expand_tabs.rs

1use alloc::{borrow::Cow, str::from_utf8_unchecked, string::String, vec::Vec};
2
3/// To extend `str` and `Cow<str>` to have `expand_tabs` method.
4///
5/// This replaces each tab with a fixed number of spaces and does not align to tab stops.
6pub trait ExpandTabs<'a> {
7    /// Returns a `Cow<str>` where each tab is replaced with `spaces` spaces.
8    ///
9    /// Passing `0` removes tabs.
10    fn expand_tabs(self, spaces: usize) -> Cow<'a, str>;
11}
12
13fn remove_tabs(s: &str) -> Cow<'_, str> {
14    let bytes = s.as_bytes();
15    let length = bytes.len();
16
17    let mut p = 0;
18
19    loop {
20        if p == length {
21            return Cow::Borrowed(s);
22        }
23
24        if bytes[p] == b'\t' {
25            break;
26        } else {
27            p += 1;
28        }
29    }
30
31    let heading_normal_characters_end_index = p;
32
33    p += 1;
34
35    // there are four situations which can use a string slice:
36    // 1. <tabs>
37    // 2. <normal_characters><tabs>
38    // 3. <tabs><normal_characters>
39    // 4. <tabs><normal_characters><tabs>
40
41    loop {
42        if p == length {
43            // situation 1 or situation 2
44
45            return Cow::Borrowed(unsafe {
46                from_utf8_unchecked(&bytes[..heading_normal_characters_end_index])
47            });
48        }
49
50        if bytes[p] == b'\t' {
51            p += 1;
52        } else {
53            break;
54        }
55    }
56
57    let following_tab_characters_end_index = p;
58
59    p += 1;
60
61    // continue to find more normal characters
62    loop {
63        if p == length {
64            if heading_normal_characters_end_index == 0 {
65                // situation 3
66                return Cow::Borrowed(unsafe {
67                    from_utf8_unchecked(&bytes[following_tab_characters_end_index..])
68                });
69            } else {
70                // <normal_characters><tabs><normal_characters>
71
72                let mut new_v = Vec::with_capacity(
73                    heading_normal_characters_end_index + length
74                        - following_tab_characters_end_index,
75                );
76
77                new_v.extend_from_slice(bytes[..heading_normal_characters_end_index].as_ref());
78                new_v.extend_from_slice(bytes[following_tab_characters_end_index..].as_ref());
79
80                return Cow::Owned(unsafe { String::from_utf8_unchecked(new_v) });
81            }
82        }
83
84        if bytes[p] == b'\t' {
85            break;
86        } else {
87            p += 1;
88        }
89    }
90
91    let following_normal_characters_end_index = p;
92
93    if p < length {
94        p += 1;
95
96        loop {
97            if p == length {
98                // situation 4
99
100                return Cow::Borrowed(unsafe {
101                    from_utf8_unchecked(
102                        &bytes[following_tab_characters_end_index
103                            ..following_normal_characters_end_index],
104                    )
105                });
106            }
107
108            if bytes[p] == b'\t' {
109                p += 1;
110            } else {
111                break;
112            }
113        }
114    }
115
116    // <tabs><normal_characters><tabs><normal_characters>XXX
117
118    let mut new_v =
119        bytes[following_tab_characters_end_index..following_normal_characters_end_index].to_vec();
120
121    let mut start = p;
122
123    p += 1;
124
125    loop {
126        if p == length {
127            break;
128        }
129
130        if bytes[p] == b'\t' {
131            new_v.extend_from_slice(&bytes[start..p]);
132
133            start = p + 1;
134        }
135
136        p += 1;
137    }
138
139    new_v.extend_from_slice(&bytes[start..p]);
140
141    Cow::Owned(unsafe { String::from_utf8_unchecked(new_v) })
142}
143
144impl<'a> ExpandTabs<'a> for &'a str {
145    fn expand_tabs(self, spaces: usize) -> Cow<'a, str> {
146        if spaces == 0 {
147            return remove_tabs(self);
148        }
149
150        let s = self;
151        let bytes = s.as_bytes();
152        let length = bytes.len();
153
154        let mut p = 0;
155
156        loop {
157            if p == length {
158                return Cow::Borrowed(s);
159            }
160
161            if bytes[p] == b'\t' {
162                break;
163            }
164
165            p += 1;
166        }
167
168        let tab_count = bytes[p..].iter().filter(|&&b| b == b'\t').count();
169        let capacity = bytes.len() + (tab_count * (spaces - 1));
170        let mut new_v = Vec::with_capacity(capacity);
171
172        new_v.extend_from_slice(&bytes[..p]);
173        new_v.resize(new_v.len() + spaces, b' ');
174
175        p += 1;
176
177        let mut start = p;
178
179        loop {
180            if p == length {
181                break;
182            }
183
184            if bytes[p] == b'\t' {
185                new_v.extend_from_slice(&bytes[start..p]);
186                new_v.resize(new_v.len() + spaces, b' ');
187
188                p += 1;
189                start = p;
190            } else {
191                p += 1;
192            }
193        }
194
195        new_v.extend_from_slice(&bytes[start..p]);
196
197        Cow::Owned(unsafe { String::from_utf8_unchecked(new_v) })
198    }
199}
200
201impl<'a> ExpandTabs<'a> for Cow<'a, str> {
202    #[inline]
203    fn expand_tabs(self, spaces: usize) -> Cow<'a, str> {
204        match self {
205            Cow::Borrowed(s) => s.expand_tabs(spaces),
206            Cow::Owned(mut s) if spaces == 0 => {
207                if s.as_bytes().contains(&b'\t') {
208                    s.retain(|c| c != '\t');
209                }
210
211                Cow::Owned(s)
212            },
213            Cow::Owned(s) => Cow::Owned(crate::cow_into_owned!(s, s.as_str().expand_tabs(spaces),)),
214        }
215    }
216}