1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
//! Lazy-copying lazy-allocated scanning [`str`] transformations. //! This is good e.g. for (un)escaping text, especially if individual strings are short. //! //! Note that this library uses [smartstring] (and as such returns [`Woc`]s instead of [`Cow`]s). //! The output is still [`Deref<Target = str>`] regardless, so there should be no issue with ease of use. //! //! # Example //! //! ```rust //! use { //! cervine::Cow, //! gnaw::Unshift as _, //! lazy_transform_str::{Transform as _, TransformedPart}, //! smartstring::alias::String, //! }; //! //! fn double_a(str: &str) -> Cow<String, str> { //! str.transform(|rest /*: &mut &str */| { //! // Consume some of the input. `rest` is never empty here. //! match rest.unshift().unwrap() { //! 'a' => TransformedPart::Changed(String::from("aa")), //! _ => TransformedPart::Unchanged, //! } //! } /*: impl FnMut(…) -> … */ ) //! } //! //! assert_eq!(double_a("abc"), Cow::Owned(String::from("aabc"))); //! assert_eq!(double_a("bcd"), Cow::Borrowed("bcd")); //! ``` //! //! See [`escape_double_quotes`] and [`unescape_backlashed_verbatim`]'s sources for more real-world examples. #![warn(clippy::pedantic)] #![doc(html_root_url = "https://docs.rs/lazy-transform-str/0.0.6")] #[cfg(doctest)] pub mod readme { doc_comment::doctest!("../README.md"); } use cervine::Cow; use gnaw::Unshift as _; use smartstring::alias::String; /// Inidicates whether the consumed part of the input remains unchanged or is to be replaced. pub enum TransformedPart { Unchanged, Changed(String), } /// Transforms the given `str` according to `transform_next` as lazily as possible. /// /// With each invocation, `transform_next` should consume part of the input (by slicing its parameter in place) and return a replacement [`String`] if necessary. /// `transform` returns once the input is an empty [`str`]. /// /// [`String`]: https://doc.rust-lang.org/stable/std/string/struct.String.html /// [`str`]: https://doc.rust-lang.org/stable/std/primitive.str.html /// /// # Example /// /// ```rust /// use cervine::Cow; /// use gnaw::Unshift as _; /// use lazy_transform_str::{transform, TransformedPart}; /// use smartstring::alias::String; /// /// let input = r#"a "quoted" word"#; /// /// // Escape double quotes /// let output = transform(input, |rest| match rest.unshift().unwrap() { /// c @ '\\' | c @ '"' => { /// let mut changed = String::from(r"\"); /// changed.push(c); /// TransformedPart::Changed(changed) /// } /// _ => TransformedPart::Unchanged, /// }); /// /// assert_eq!(output, Cow::Owned(r#"a \"quoted\" word"#.into())); /// ``` pub fn transform( str: &str, transform_next: impl FnMut(/* rest: */ &mut &str) -> TransformedPart, ) -> Cow<String, str> { str.transform(transform_next) } /// Helper trait to call [`transform`] as method on [`&str`]. /// /// [`transform`]: ./fn.transform.html /// [`&str`]: https://doc.rust-lang.org/stable/std/primitive.str.html /// /// # Example /// /// ```rust /// use cervine::Cow; /// use gnaw::Unshift as _; /// use lazy_transform_str::{Transform as _, TransformedPart}; /// use smartstring::alias::String; /// /// let input = r#"a "quoted" word"#; /// /// // Escape double quotes /// let output = input.transform(|rest| match rest.unshift().unwrap() { /// c @ '\\' | c @ '"' => { /// let mut changed = String::from(r"\"); /// changed.push(c); /// TransformedPart::Changed(changed) /// } /// _ => TransformedPart::Unchanged, /// }); /// /// assert_eq!(output, Cow::Owned(r#"a \"quoted\" word"#.into())); /// ``` pub trait Transform { fn transform( &self, transform_next: impl FnMut(&mut &str) -> TransformedPart, ) -> Cow<String, str>; } impl Transform for str { fn transform( &self, mut transform_next: impl FnMut(&mut &str) -> TransformedPart, ) -> Cow<String, str> { let mut rest = self; let mut copied = loop { if rest.is_empty() { return Cow::Borrowed(self); } let unchanged_rest = rest; if let TransformedPart::Changed(transformed) = transform_next(&mut rest) { let mut copied = String::from(&self[..self.len() - unchanged_rest.len()]); copied.push_str(&transformed); break copied; } }; while !rest.is_empty() { let unchanged_rest = rest; match transform_next(&mut rest) { TransformedPart::Unchanged => { copied.push_str(&unchanged_rest[..unchanged_rest.len() - rest.len()]); } TransformedPart::Changed(changed) => copied.push_str(&changed), } } Cow::Owned(copied) } } /// Replaces `\` and `"` in `string` with (repectively) `\\` and `\"`, as lazily as possible. /// /// # Example /// /// ```rust /// use cervine::Cow; /// use lazy_transform_str::escape_double_quotes; /// /// let input = r#"a "quoted" word"#; /// /// let output = escape_double_quotes(input); /// /// assert_eq!(output, Cow::Owned(r#"a \"quoted\" word"#.into())); /// ``` #[must_use = "pure function"] pub fn escape_double_quotes(string: &str) -> Cow<String, str> { string.transform(|rest| match rest.unshift().unwrap() { c @ '\\' | c @ '"' => { let mut changed = String::from(r"\"); changed.push(c); TransformedPart::Changed(changed) } _ => TransformedPart::Unchanged, }) } /// Replaces `\` followed by any Unicode [`char`] in `string` with that [`char`], as lazily as possible. /// If `\\` is found, this sequence is consumed at once and a single `\` remains in the output. /// /// [`char`]: https://doc.rust-lang.org/stable/std/primitive.char.html /// /// # Example /// /// ```rust /// use cervine::Cow; /// use lazy_transform_str::unescape_backslashed_verbatim; /// /// let input = r#"A \"quoted\" word\\!"#; /// /// let output = unescape_backslashed_verbatim(input); /// /// assert_eq!(output, Cow::Owned(r#"A "quoted" word\!"#.into())); /// /// let output = unescape_backslashed_verbatim(&output); /// /// assert_eq!(output, Cow::Owned(r#"A "quoted" word!"#.into())); /// ``` #[must_use = "pure function"] pub fn unescape_backslashed_verbatim(string: &str) -> Cow<String, str> { let mut escaped = false; string.transform(|rest| match rest.unshift().unwrap() { '\\' if !escaped => { escaped = true; TransformedPart::Changed(String::new()) } _ => { escaped = false; TransformedPart::Unchanged } }) }