str-utils 0.3.7

This crate provides some traits to extend `[u8]`, `str` and `Cow<str>`.
Documentation
use alloc::{borrow::Cow, str::from_utf8_unchecked, string::String, vec::Vec};

/// To extend `str` and `Cow<str>` to have `expand_tabs` method.
///
/// This replaces each tab with a fixed number of spaces and does not align to tab stops.
pub trait ExpandTabs<'a> {
    /// Returns a `Cow<str>` where each tab is replaced with `spaces` spaces.
    ///
    /// Passing `0` removes tabs.
    fn expand_tabs(self, spaces: usize) -> Cow<'a, str>;
}

fn remove_tabs(s: &str) -> Cow<'_, str> {
    let bytes = s.as_bytes();
    let length = bytes.len();

    let mut p = 0;

    loop {
        if p == length {
            return Cow::Borrowed(s);
        }

        if bytes[p] == b'\t' {
            break;
        } else {
            p += 1;
        }
    }

    let heading_normal_characters_end_index = p;

    p += 1;

    // there are four situations which can use a string slice:
    // 1. <tabs>
    // 2. <normal_characters><tabs>
    // 3. <tabs><normal_characters>
    // 4. <tabs><normal_characters><tabs>

    loop {
        if p == length {
            // situation 1 or situation 2

            return Cow::Borrowed(unsafe {
                from_utf8_unchecked(&bytes[..heading_normal_characters_end_index])
            });
        }

        if bytes[p] == b'\t' {
            p += 1;
        } else {
            break;
        }
    }

    let following_tab_characters_end_index = p;

    p += 1;

    // continue to find more normal characters
    loop {
        if p == length {
            if heading_normal_characters_end_index == 0 {
                // situation 3
                return Cow::Borrowed(unsafe {
                    from_utf8_unchecked(&bytes[following_tab_characters_end_index..])
                });
            } else {
                // <normal_characters><tabs><normal_characters>

                let mut new_v = Vec::with_capacity(
                    heading_normal_characters_end_index + length
                        - following_tab_characters_end_index,
                );

                new_v.extend_from_slice(bytes[..heading_normal_characters_end_index].as_ref());
                new_v.extend_from_slice(bytes[following_tab_characters_end_index..].as_ref());

                return Cow::Owned(unsafe { String::from_utf8_unchecked(new_v) });
            }
        }

        if bytes[p] == b'\t' {
            break;
        } else {
            p += 1;
        }
    }

    let following_normal_characters_end_index = p;

    if p < length {
        p += 1;

        loop {
            if p == length {
                // situation 4

                return Cow::Borrowed(unsafe {
                    from_utf8_unchecked(
                        &bytes[following_tab_characters_end_index
                            ..following_normal_characters_end_index],
                    )
                });
            }

            if bytes[p] == b'\t' {
                p += 1;
            } else {
                break;
            }
        }
    }

    // <tabs><normal_characters><tabs><normal_characters>XXX

    let mut new_v =
        bytes[following_tab_characters_end_index..following_normal_characters_end_index].to_vec();

    let mut start = p;

    p += 1;

    loop {
        if p == length {
            break;
        }

        if bytes[p] == b'\t' {
            new_v.extend_from_slice(&bytes[start..p]);

            start = p + 1;
        }

        p += 1;
    }

    new_v.extend_from_slice(&bytes[start..p]);

    Cow::Owned(unsafe { String::from_utf8_unchecked(new_v) })
}

impl<'a> ExpandTabs<'a> for &'a str {
    fn expand_tabs(self, spaces: usize) -> Cow<'a, str> {
        if spaces == 0 {
            return remove_tabs(self);
        }

        let s = self;
        let bytes = s.as_bytes();
        let length = bytes.len();

        let mut p = 0;

        loop {
            if p == length {
                return Cow::Borrowed(s);
            }

            if bytes[p] == b'\t' {
                break;
            }

            p += 1;
        }

        let tab_count = bytes[p..].iter().filter(|&&b| b == b'\t').count();
        let capacity = bytes.len() + (tab_count * (spaces - 1));
        let mut new_v = Vec::with_capacity(capacity);

        new_v.extend_from_slice(&bytes[..p]);
        new_v.resize(new_v.len() + spaces, b' ');

        p += 1;

        let mut start = p;

        loop {
            if p == length {
                break;
            }

            if bytes[p] == b'\t' {
                new_v.extend_from_slice(&bytes[start..p]);
                new_v.resize(new_v.len() + spaces, b' ');

                p += 1;
                start = p;
            } else {
                p += 1;
            }
        }

        new_v.extend_from_slice(&bytes[start..p]);

        Cow::Owned(unsafe { String::from_utf8_unchecked(new_v) })
    }
}

impl<'a> ExpandTabs<'a> for Cow<'a, str> {
    #[inline]
    fn expand_tabs(self, spaces: usize) -> Cow<'a, str> {
        match self {
            Cow::Borrowed(s) => s.expand_tabs(spaces),
            Cow::Owned(mut s) if spaces == 0 => {
                if s.as_bytes().contains(&b'\t') {
                    s.retain(|c| c != '\t');
                }

                Cow::Owned(s)
            },
            Cow::Owned(s) => Cow::Owned(crate::cow_into_owned!(s, s.as_str().expand_tabs(spaces),)),
        }
    }
}