text-fx 0.4.0

A collection of text processing utilities for Rust.
Documentation
use std::fmt::Write;

/// The style of quoting to use: single or double quotes.
pub enum QuoteStyle {
    /// Use single quotes (e.g., `'value'`)
    Single,
    /// Use double quotes (e.g., `"value"`)
    Double,
}

/// A wrapper for a value to be quoted with a given style.
///
/// This struct is used to format a value (typically a byte slice or string)
/// with the specified quoting style and appropriate escaping for display or diagnostics.
///
/// Use the [`quoted`], [`qq`], or [`q`] functions to construct a `Quoted` value.
///
/// # Examples
///
/// ```
/// use text_fx::quote::{qq, q, quoted, QuoteStyle};
///
/// let s = b"foo\nbar\"baz";
/// assert_eq!(qq(s).to_string(), "\"foo\\nbar\\\"baz\"");
/// assert_eq!(q(s).to_string(), "'foo\\nbar\"baz'");
///
/// let custom = quoted(s, QuoteStyle::Single);
/// assert_eq!(custom.to_string(), "'foo\\nbar\"baz'");
/// ```
pub struct Quoted<T> {
    style: QuoteStyle,
    value: T,
}

impl<T: AsRef<[u8]>> std::fmt::Display for Quoted<T> {
    /// Format the value with the appropriate quoting and escaping.
    ///
    /// - Escapes quotes, backslashes, and control characters.
    /// - Non-printable bytes are escaped as `\xNN`.
    /// - Printable ASCII and spaces are left as-is.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.style {
            QuoteStyle::Single => write!(f, "'")?,
            QuoteStyle::Double => write!(f, "\"")?,
        }

        for &b in self.value.as_ref() {
            match (&self.style, b) {
                // Escape single quote inside single-quoted string
                (QuoteStyle::Single, b'\'') => f.write_str("'\\''")?,
                // Escape double quote inside double-quoted string
                (QuoteStyle::Double, b'"') => f.write_str("\\\"")?,
                // Escape backslash
                (_, b'\\') => f.write_str("\\\\")?,
                // Escape common control characters
                (_, b'\n') => f.write_str("\\n")?,
                (_, b'\r') => f.write_str("\\r")?,
                (_, b'\t') => f.write_str("\\t")?,
                // Printable ASCII or space
                (_, b) if b.is_ascii_graphic() || b == b' ' => f.write_char(b as char)?,
                // Escape all other bytes as hex
                (_, b) => write!(f, "\\x{:02x}", b)?,
            }
        }

        match self.style {
            QuoteStyle::Single => write!(f, "'"),
            QuoteStyle::Double => write!(f, "\""),
        }
    }
}

/// Return a `Quoted` wrapper for the given value and quote style.
///
/// # Examples
///
/// ```
/// use text_fx::quote::{quoted, QuoteStyle};
/// let s = b"foo";
/// let q = quoted(s, QuoteStyle::Double);
/// assert_eq!(q.to_string(), "\"foo\"");
/// ```
pub fn quoted<T: AsRef<[u8]>>(value: T, style: QuoteStyle) -> Quoted<T> {
    Quoted { value, style }
}

/// Return a `Quoted` wrapper using single quotes for diagnostic quoting.
///
/// # Examples
///
/// ```
/// use text_fx::quote::q;
/// let s = b"foo";
/// assert_eq!(q(s).to_string(), "'foo'");
/// ```
pub fn q<B: AsRef<[u8]>>(value: B) -> Quoted<B> {
    Quoted {
        value,
        style: QuoteStyle::Single,
    }
}

/// Return a `Quoted` wrapper using double quotes (default).
///
/// # Examples
///
/// ```
/// use text_fx::quote::qq;
/// let s = b"foo";
/// assert_eq!(qq(s).to_string(), "\"foo\"");
/// ```
pub fn qq<B: AsRef<[u8]>>(value: B) -> Quoted<B> {
    Quoted {
        value,
        style: QuoteStyle::Double,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_double() {
        // Test double-quoted output with escaped newline and double quote
        let p = b"foo\nbar\"baz";
        let s = Quoted {
            style: QuoteStyle::Double,
            value: p.as_slice(),
        }
        .to_string();
        let expected = "\"foo\\nbar\\\"baz\"";
        assert_eq!(s, expected);

        // Test double-quoted output with backslash and non-printable byte
        let data = b"abc\\def\x01";
        let quoted = qq(data).to_string();
        let expected = "\"abc\\\\def\\x01\"";
        assert_eq!(quoted, expected);
    }

    #[test]
    fn test_empty_and_all_ascii() {
        // Test empty string
        let empty = b"";
        assert_eq!(qq(empty).to_string(), "\"\"");

        // Test all printable ASCII characters
        let ascii: Vec<u8> = (0x20..=0x7e).collect();
        let quoted = qq(&ascii).to_string();
        let expected = format!("{:?}", String::from_utf8_lossy(&ascii));
        assert_eq!(quoted, expected);
    }

    #[test]
    fn test_control_and_non_ascii() {
        // Test string with various control and non-ASCII bytes
        let bytes = b"\x00\x1f\x7f\xc0";
        let quoted = quoted(bytes, QuoteStyle::Double).to_string();
        let expected = "\"\\x00\\x1f\\x7f\\xc0\"";
        assert_eq!(quoted, expected);
    }
}