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