merge_whitespace_utils/
lib.rs

1//! # merge-whitespace-utils
2//!
3//! This crate contains the [`merge_whitespace`] and [`merge_whitespace_with_quotes`] functions
4//! for removing multiple consecutive whitespaces from a given string, replacing them with a single space.
5//!
6//! ## Example
7//!
8//! ```
9//! # use merge_whitespace_utils::merge_whitespace_with_quotes;
10//! let query = merge_whitespace_with_quotes(r#"
11//!                 query {
12//!                   users (limit: 1, filter: "bought a 12\" vinyl
13//!                                             named \"spaces  in  space \"") {
14//!                     id
15//!                     name
16//!                     todos(order_by: {created_at: desc}, limit: 5) {
17//!                       id
18//!                       title
19//!                     }
20//!                   }
21//!                 }
22//!                 "#,
23//!                 Some('"'),
24//!                 Some('\\'));
25//!
26//! assert_eq!(query, r#"query { users (limit: 1, filter: "bought a 12\" vinyl
27//!                                             named \"spaces  in  space \"") { id name todos(order_by: {created_at: desc}, limit: 5) { id title } } }"#);
28//! ```
29
30#![forbid(unsafe_code)]
31
32use std::borrow::Cow;
33
34/// Remove multiple consecutive whitespaces from a given string and replace them with a single space.
35/// If special handling of quoted text is required, see [`merge_whitespace_with_quotes`] instead.
36///
37/// ## Example
38///
39/// ```
40/// # use merge_whitespace_utils::merge_whitespace;
41/// let output = merge_whitespace("Hello     World!\r\n      \"How        are\"         you?");
42/// assert_eq!(output, r#"Hello World! "How are" you?"#);
43/// ```
44///
45/// # Return
46///
47/// The modified string.
48pub fn merge_whitespace(input: &str) -> Cow<str> {
49    merge_whitespace_with_quotes(input, None, None)
50}
51
52/// Remove multiple consecutive whitespaces from a given string literal and replace them with a
53/// single space. Quoted text will be ignored and kept as-is.
54///
55/// ## Example
56///
57/// ```
58/// # use merge_whitespace_utils::merge_whitespace_with_quotes;
59/// let output = merge_whitespace_with_quotes("Hello     World!\r\n      \"How        are\"         you?", Some('"'), None);
60/// assert_eq!(output, "Hello World! \"How        are\" you?");
61/// ```
62///
63/// # Return
64///
65/// The modified string.
66pub fn merge_whitespace_with_quotes(
67    input: &str,
68    quote_char: Option<char>,
69    escape_char: Option<char>,
70) -> Cow<str> {
71    let trimmed_input = input.trim();
72    let mut result = None; // Use this to lazily initialize a String if needed
73    let mut in_quotes = false;
74    let mut prev_char_was_space = false;
75    let mut in_escape = false;
76
77    for c in trimmed_input.chars() {
78        if escape_char == Some(c) && !in_escape {
79            if prev_char_was_space {
80                result
81                    .get_or_insert_with(|| String::with_capacity(trimmed_input.len()))
82                    .push(' ');
83            }
84            prev_char_was_space = false;
85            in_escape = true;
86            result
87                .get_or_insert_with(|| String::with_capacity(trimmed_input.len()))
88                .push(c);
89            continue;
90        }
91        if c.is_whitespace() && !in_quotes && !in_escape {
92            prev_char_was_space = true;
93            continue;
94        }
95        if quote_char == Some(c) && !in_escape {
96            in_quotes = !in_quotes;
97        }
98        if prev_char_was_space {
99            result
100                .get_or_insert_with(|| String::with_capacity(trimmed_input.len()))
101                .push(' ');
102        }
103        result
104            .get_or_insert_with(|| String::with_capacity(trimmed_input.len()))
105            .push(c);
106        prev_char_was_space = false;
107        in_escape = false;
108    }
109
110    match result {
111        Some(resulting_string) => Cow::Owned(resulting_string),
112        None => Cow::Borrowed(trimmed_input),
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    const QUOTE: Option<char> = Some('"');
121    const ESCAPE: Option<char> = Some('\\');
122
123    #[test]
124    fn whitespace_only_is_trimmed() {
125        assert_eq!(
126            merge_whitespace_with_quotes("  ", QUOTE, None),
127            Cow::Borrowed("")
128        );
129        assert_eq!(
130            merge_whitespace_with_quotes("  \n \t  ", QUOTE, None),
131            Cow::Borrowed("")
132        );
133    }
134
135    #[test]
136    fn non_whitespace_is_ignored() {
137        assert_eq!(
138            merge_whitespace_with_quotes("abcdefgh.ihkl-", QUOTE, None),
139            Cow::Borrowed("abcdefgh.ihkl-")
140        );
141    }
142
143    #[test]
144    fn single_whitespace_in_text_is_kept() {
145        assert_eq!(
146            merge_whitespace_with_quotes("foo bar baz", QUOTE, None),
147            Cow::Borrowed("foo bar baz")
148        );
149    }
150
151    #[test]
152    fn multiple_whitespace_in_text_is_merged() {
153        assert_eq!(
154            merge_whitespace_with_quotes("foo  bar\nbaz", QUOTE, None),
155            Cow::<str>::Owned(String::from("foo bar baz"))
156        );
157    }
158
159    #[test]
160    fn quoted_whitespace_in_text_is_kept() {
161        assert_eq!(
162            merge_whitespace_with_quotes("foo   foobar   \"  bar\n\" baz", QUOTE, None),
163            Cow::<str>::Owned(String::from("foo foobar \"  bar\n\" baz"))
164        );
165    }
166
167    #[test]
168    fn escape_a_space() {
169        assert_eq!(
170            merge_whitespace_with_quotes("what   \\   if I quote\\ spaces", QUOTE, ESCAPE),
171            Cow::<str>::Owned(String::from("what \\  if I quote\\ spaces"))
172        );
173    }
174
175    #[test]
176    fn quoted_whitespace_with_escaped_quotes() {
177        assert_eq!(
178            merge_whitespace_with_quotes(
179                r#"foo   foobar   "  \"bar   \"   "   baz"#,
180                QUOTE,
181                ESCAPE
182            ),
183            Cow::<str>::Owned(String::from(r#"foo foobar "  \"bar   \"   " baz"#))
184        );
185    }
186
187    #[test]
188    fn test_complex_escaped() {
189        let result = merge_whitespace_with_quotes(
190            r#"
191                query {
192                  users (limit: 1, name: "Froozle   '78\"'   Frobnik") {
193                    id
194                    name
195                    todos(order_by: {created_at: desc}, limit: 5) {
196                      id
197                      title
198                    }
199                  }
200                }
201                "#,
202            QUOTE,
203            ESCAPE,
204        );
205        assert_eq!(result, "query { users (limit: 1, name: \"Froozle   '78\\\"'   Frobnik\") { id name todos(order_by: {created_at: desc}, limit: 5) { id title } } }");
206    }
207
208    #[test]
209    fn test_complex_unescaped() {
210        let result = merge_whitespace_with_quotes(
211            r#"
212                query {
213                  users (limit: 1, name: "Froozle   Frobnik") {
214                    id
215                    name
216                    todos(order_by: {created_at: desc}, limit: 5) {
217                      id
218                      title
219                    }
220                  }
221                }
222                "#,
223            QUOTE,
224            None,
225        );
226        assert_eq!(result, "query { users (limit: 1, name: \"Froozle   Frobnik\") { id name todos(order_by: {created_at: desc}, limit: 5) { id title } } }");
227    }
228}