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