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}