lazy_transform_str/
lib.rs

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)]
37pub mod readme {
38	doc_comment::doctest!("../README.md");
39}
40
41use cervine::Cow;
42use gnaw::Unshift as _;
43use smartstring::alias::String;
44
45/// Inidicates whether the consumed part of the input remains unchanged or is to be replaced.
46pub 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/// ```
81pub 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/// ```
115pub trait Transform {
116	fn transform(
117		&self,
118		transform_next: impl FnMut(&mut &str) -> TransformedPart,
119	) -> Cow<String, str>;
120}
121
122impl Transform for 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"]
169pub 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"]
202pub 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}