text_fx/
quote.rs

1use std::fmt::Write;
2
3/// The style of quoting to use: single or double quotes.
4pub enum QuoteStyle {
5    /// Use single quotes (e.g., `'value'`)
6    Single,
7    /// Use double quotes (e.g., `"value"`)
8    Double,
9}
10
11/// A wrapper for a value to be quoted with a given style.
12///
13/// This struct is used to format a value (typically a byte slice or string)
14/// with the specified quoting style and appropriate escaping for display or diagnostics.
15///
16/// Use the [`quoted`], [`qq`], or [`q`] functions to construct a `Quoted` value.
17///
18/// # Examples
19///
20/// ```
21/// use text_fx::quote::{qq, q, quoted, QuoteStyle};
22///
23/// let s = b"foo\nbar\"baz";
24/// assert_eq!(qq(s).to_string(), "\"foo\\nbar\\\"baz\"");
25/// assert_eq!(q(s).to_string(), "'foo\\nbar\"baz'");
26///
27/// let custom = quoted(s, QuoteStyle::Single);
28/// assert_eq!(custom.to_string(), "'foo\\nbar\"baz'");
29/// ```
30pub struct Quoted<T> {
31    style: QuoteStyle,
32    value: T,
33}
34
35impl<T: AsRef<[u8]>> std::fmt::Display for Quoted<T> {
36    /// Format the value with the appropriate quoting and escaping.
37    ///
38    /// - Escapes quotes, backslashes, and control characters.
39    /// - Non-printable bytes are escaped as `\xNN`.
40    /// - Printable ASCII and spaces are left as-is.
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        match self.style {
43            QuoteStyle::Single => write!(f, "'")?,
44            QuoteStyle::Double => write!(f, "\"")?,
45        }
46
47        for &b in self.value.as_ref() {
48            match (&self.style, b) {
49                // Escape single quote inside single-quoted string
50                (QuoteStyle::Single, b'\'') => f.write_str("'\\''")?,
51                // Escape double quote inside double-quoted string
52                (QuoteStyle::Double, b'"') => f.write_str("\\\"")?,
53                // Escape backslash
54                (_, b'\\') => f.write_str("\\\\")?,
55                // Escape common control characters
56                (_, b'\n') => f.write_str("\\n")?,
57                (_, b'\r') => f.write_str("\\r")?,
58                (_, b'\t') => f.write_str("\\t")?,
59                // Printable ASCII or space
60                (_, b) if b.is_ascii_graphic() || b == b' ' => f.write_char(b as char)?,
61                // Escape all other bytes as hex
62                (_, b) => write!(f, "\\x{:02x}", b)?,
63            }
64        }
65
66        match self.style {
67            QuoteStyle::Single => write!(f, "'"),
68            QuoteStyle::Double => write!(f, "\""),
69        }
70    }
71}
72
73/// Return a `Quoted` wrapper for the given value and quote style.
74///
75/// # Examples
76///
77/// ```
78/// use text_fx::quote::{quoted, QuoteStyle};
79/// let s = b"foo";
80/// let q = quoted(s, QuoteStyle::Double);
81/// assert_eq!(q.to_string(), "\"foo\"");
82/// ```
83pub fn quoted<T: AsRef<[u8]>>(value: T, style: QuoteStyle) -> Quoted<T> {
84    Quoted { value, style }
85}
86
87/// Return a `Quoted` wrapper using single quotes for diagnostic quoting.
88///
89/// # Examples
90///
91/// ```
92/// use text_fx::quote::q;
93/// let s = b"foo";
94/// assert_eq!(q(s).to_string(), "'foo'");
95/// ```
96pub fn q<B: AsRef<[u8]>>(value: B) -> Quoted<B> {
97    Quoted {
98        value,
99        style: QuoteStyle::Single,
100    }
101}
102
103/// Return a `Quoted` wrapper using double quotes (default).
104///
105/// # Examples
106///
107/// ```
108/// use text_fx::quote::qq;
109/// let s = b"foo";
110/// assert_eq!(qq(s).to_string(), "\"foo\"");
111/// ```
112pub fn qq<B: AsRef<[u8]>>(value: B) -> Quoted<B> {
113    Quoted {
114        value,
115        style: QuoteStyle::Double,
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_double() {
125        // Test double-quoted output with escaped newline and double quote
126        let p = b"foo\nbar\"baz";
127        let s = Quoted {
128            style: QuoteStyle::Double,
129            value: p.as_slice(),
130        }
131        .to_string();
132        let expected = "\"foo\\nbar\\\"baz\"";
133        assert_eq!(s, expected);
134
135        // Test double-quoted output with backslash and non-printable byte
136        let data = b"abc\\def\x01";
137        let quoted = qq(data).to_string();
138        let expected = "\"abc\\\\def\\x01\"";
139        assert_eq!(quoted, expected);
140    }
141
142    #[test]
143    fn test_empty_and_all_ascii() {
144        // Test empty string
145        let empty = b"";
146        assert_eq!(qq(empty).to_string(), "\"\"");
147
148        // Test all printable ASCII characters
149        let ascii: Vec<u8> = (0x20..=0x7e).collect();
150        let quoted = qq(&ascii).to_string();
151        let expected = format!("{:?}", String::from_utf8_lossy(&ascii));
152        assert_eq!(quoted, expected);
153    }
154
155    #[test]
156    fn test_control_and_non_ascii() {
157        // Test string with various control and non-ASCII bytes
158        let bytes = b"\x00\x1f\x7f\xc0";
159        let quoted = quoted(bytes, QuoteStyle::Double).to_string();
160        let expected = "\"\\x00\\x1f\\x7f\\xc0\"";
161        assert_eq!(quoted, expected);
162    }
163}