1use std::fmt::{self, Write};
2
3fn write_character(c: char, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5 match c {
6 '"' => f.write_str(r#"\""#),
7 '\u{0008}' => f.write_str(r"\b"),
8 '\u{000c}' => f.write_str(r"\f"),
9 '\n' => f.write_str(r"\n"),
10 '\r' => f.write_str(r"\r"),
11 '\t' => f.write_str(r"\t"),
12 '\\' => f.write_str(r"\\"),
13 c if c.is_control() => write!(f, "\\u{:04x}", c as u32),
14 c => write!(f, "{c}"),
16 }
17}
18
19#[derive(Debug)]
21struct BlockStringFormatter<'a> {
22 string: &'a str,
23 indent: usize,
26}
27
28fn write_block_string_line(line: &'_ str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 let mut char_iter = line.char_indices();
31
32 while let Some((pos, c)) = char_iter.next() {
33 if c == '"' && line.get(pos..pos + 3) == Some("\"\"\"") {
35 char_iter.next();
38 char_iter.next();
39
40 f.write_str("\\\"\"\"")?;
41 continue;
42 }
43
44 f.write_char(c)?;
45 }
46
47 Ok(())
48}
49
50impl fmt::Display for BlockStringFormatter<'_> {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 let indent = " ".repeat(self.indent);
53
54 write!(f, "{indent}\"\"\"")?;
55 for line in self.string.lines() {
56 write!(f, "\n{indent}")?;
57 write_block_string_line(line, f)?;
58 }
59 write!(f, "\n{indent}\"\"\"")?;
60
61 Ok(())
62 }
63}
64
65#[derive(Debug)]
67struct StringFormatter<'a>(&'a str);
68impl fmt::Display for StringFormatter<'_> {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.write_char('"')?;
71 for c in self.0.chars() {
72 write_character(c, f)?;
73 }
74 f.write_char('"')?;
75 Ok(())
76 }
77}
78
79#[derive(Debug, PartialEq, Eq, Clone)]
80pub enum StringValue {
84 Top {
86 source: String,
88 },
89 Field {
92 source: String,
94 },
95 Input {
98 source: String,
100 },
101}
102
103impl fmt::Display for StringValue {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 let source = match self {
106 StringValue::Top { source }
107 | StringValue::Field { source }
108 | StringValue::Input { source } => source,
109 };
110 let indent = match self {
113 StringValue::Top { .. } => 0,
114 StringValue::Field { .. } => 2,
115 StringValue::Input { .. } => 4,
116 };
117
118 if should_use_block_string(source) {
119 write!(
120 f,
121 "{}",
122 BlockStringFormatter {
123 string: source,
124 indent,
125 }
126 )
127 } else {
128 write!(
131 f,
132 "{:indent$}{string}",
133 "",
134 indent = indent,
135 string = StringFormatter(source),
136 )
137 }
138 }
139}
140
141fn should_use_block_string(s: &str) -> bool {
144 s.contains(['"', '\n']) && s.lines().all(|line| !line.contains(char::is_control))
145}
146
147#[cfg(test)]
148mod test {
149 use super::*;
150 use pretty_assertions::assert_eq;
151
152 #[test]
153 fn it_encodes_description_without_block_string_character() {
154 let desc = StringValue::Top {
155 source: "Favourite cat nap spots include: plant corner, pile of clothes.".to_string(),
156 };
157
158 assert_eq!(
159 desc.to_string(),
160 r#""Favourite cat nap spots include: plant corner, pile of clothes.""#
161 );
162 }
163
164 #[test]
165 fn it_encodes_description_with_quotations() {
166 let desc = StringValue::Top {
167 source: r#""Favourite "cat" nap spots include: plant corner, pile of clothes.""#
168 .to_string(),
169 };
170
171 assert_eq!(
172 desc.to_string(),
173 r#""""
174"Favourite "cat" nap spots include: plant corner, pile of clothes."
175""""#
176 );
177 }
178
179 #[test]
180 fn it_encodes_description_with_other_languages() {
181 let desc = StringValue::Top {
182 source: r#"котя(猫, ねこ, قطة) любить дрімати в "кутку" з рослинами"#.to_string(),
183 };
184
185 assert_eq!(
186 desc.to_string(),
187 r#""""
188котя(猫, ねこ, قطة) любить дрімати в "кутку" з рослинами
189""""#
190 );
191 }
192
193 #[test]
194 fn it_encodes_description_with_new_line() {
195 let desc = StringValue::Top {
196 source: "Favourite cat nap spots include:\nplant corner, pile of clothes.".to_string(),
197 };
198
199 assert_eq!(
200 desc.to_string(),
201 r#""""
202Favourite cat nap spots include:
203plant corner, pile of clothes.
204""""#
205 );
206 }
207
208 #[test]
209 fn it_encodes_description_with_carriage_return() {
210 let desc = StringValue::Top {
211 source: "Favourite cat nap spots include:\rplant corner,\rpile of clothes.".to_string(),
212 };
213
214 assert_eq!(
215 desc.to_string(),
216 r#""Favourite cat nap spots include:\rplant corner,\rpile of clothes.""#,
217 );
218 }
219
220 #[test]
221 fn it_encodes_indented_desciption() {
222 let desc = StringValue::Field {
223 source: "Favourite cat nap spots include:\n plant corner,\n pile of clothes."
224 .to_string(),
225 };
226
227 assert_eq!(
228 desc.to_string(),
229 r#" """
230 Favourite cat nap spots include:
231 plant corner,
232 pile of clothes.
233 """"#,
234 );
235
236 let desc = StringValue::Field {
237 source: "Favourite cat nap spots include:\r\n plant corner,\r\n pile of clothes."
238 .to_string(),
239 };
240
241 assert_eq!(
242 desc.to_string(),
243 r#" """
244 Favourite cat nap spots include:
245 plant corner,
246 pile of clothes.
247 """"#,
248 );
249
250 let desc = StringValue::Field {
251 source: "Favourite cat nap spots include:\r plant corner,\r pile of clothes."
252 .to_string(),
253 };
254
255 assert_eq!(
256 desc.to_string(),
257 r#" "Favourite cat nap spots include:\r plant corner,\r pile of clothes.""#,
258 );
259 }
260
261 #[test]
262 fn it_encodes_ends_with_quote() {
263 let source = r#"ends with ""#.to_string();
264
265 let desc = StringValue::Top {
266 source: source.clone(),
267 };
268 assert_eq!(
269 desc.to_string(),
270 r#""""
271ends with "
272""""#
273 );
274
275 let desc = StringValue::Field {
276 source: source.clone(),
277 };
278 assert_eq!(
279 desc.to_string(),
280 r#" """
281 ends with "
282 """"#
283 );
284
285 let desc = StringValue::Input { source };
286 assert_eq!(
287 desc.to_string(),
288 r#" """
289 ends with "
290 """"#
291 );
292 }
293
294 #[test]
295 fn it_encodes_triple_quotes() {
296 let source = r#"this """ has """ triple """ quotes"#.to_string();
297
298 let desc = StringValue::Top { source };
299 assert_eq!(
300 desc.to_string(),
301 r#""""
302this \""" has \""" triple \""" quotes
303""""#
304 );
305
306 let source = r#"this """ has """" many """"""" quotes"#.to_string();
307
308 let desc = StringValue::Top { source };
309 println!("{desc}");
310 assert_eq!(
311 desc.to_string(),
312 r#""""
313this \""" has \"""" many \"""\"""" quotes
314""""#
315 );
316 }
317
318 #[test]
319 fn it_encodes_control_characters() {
320 let source = "control \u{009c} character".to_string();
321
322 let desc = StringValue::Top { source };
323 assert_eq!(desc.to_string(), r#""control \u009c character""#);
324
325 let source = "multi-line\nwith control \u{009c} character\n :)".to_string();
326
327 let desc = StringValue::Top { source };
328 println!("{desc}");
329 assert_eq!(
330 desc.to_string(),
331 r#""multi-line\nwith control \u009c character\n :)""#
332 );
333 }
334}