1use std::fmt::Write as _;
16
17use crate::printable::is_printable;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum Str {
35 Utf8(Box<str>),
37 Wide(Box<[u32]>),
39}
40
41impl Str {
42 pub fn code_points(&self) -> impl Iterator<Item = u32> + '_ {
44 let (text, wide) = match self {
47 Str::Utf8(s) => (Some(s.chars()), None),
48 Str::Wide(w) => (None, Some(w.iter().copied())),
49 };
50 text.into_iter()
51 .flatten()
52 .map(u32::from)
53 .chain(wide.into_iter().flatten())
54 }
55
56 #[must_use]
58 pub fn repr(&self) -> String {
59 match self {
60 Str::Utf8(s) => str_repr(s),
61 Str::Wide(w) => repr_code_points(w.iter().copied(), w.len()),
62 }
63 }
64
65 #[must_use]
72 pub fn len(&self) -> usize {
73 match self {
74 Str::Utf8(s) if s.is_ascii() => s.len(),
77 Str::Utf8(s) => s.chars().count(),
78 Str::Wide(w) => w.len(),
79 }
80 }
81
82 #[must_use]
89 pub fn code_point_at(&self, index: usize) -> Option<u32> {
90 match self {
91 Str::Utf8(s) if s.is_ascii() => s.as_bytes().get(index).copied().map(u32::from),
92 Str::Utf8(s) => s.chars().nth(index).map(u32::from),
93 Str::Wide(w) => w.get(index).copied(),
94 }
95 }
96
97 #[must_use]
99 pub fn is_empty(&self) -> bool {
100 match self {
101 Str::Utf8(s) => s.is_empty(),
102 Str::Wide(w) => w.is_empty(),
103 }
104 }
105}
106
107impl std::fmt::Display for Str {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 match self {
116 Str::Utf8(s) => f.write_str(s),
117 Str::Wide(w) => w
118 .iter()
119 .map(|&cp| char::from_u32(cp).unwrap_or(char::REPLACEMENT_CHARACTER))
120 .try_for_each(|c| f.write_char(c)),
121 }
122 }
123}
124
125impl From<&str> for Str {
126 fn from(s: &str) -> Self {
127 Str::Utf8(s.into())
128 }
129}
130
131impl From<String> for Str {
132 fn from(s: String) -> Self {
133 Str::Utf8(s.into_boxed_str())
134 }
135}
136
137#[derive(Debug, Default)]
145pub struct StrBuf {
146 text: String,
147 wide: Option<Vec<u32>>,
149}
150
151impl StrBuf {
152 #[must_use]
153 pub fn new() -> Self {
154 Self::default()
155 }
156
157 pub fn push(&mut self, c: char) {
158 match &mut self.wide {
159 Some(wide) => wide.push(u32::from(c)),
160 None => self.text.push(c),
161 }
162 }
163
164 pub fn push_str(&mut self, s: &str) {
165 match &mut self.wide {
166 Some(wide) => wide.extend(s.chars().map(u32::from)),
167 None => self.text.push_str(s),
168 }
169 }
170
171 pub fn push_code_point(&mut self, cp: u32) {
176 if let Some(c) = char::from_u32(cp) {
177 self.push(c);
178 return;
179 }
180 self.widen().push(cp);
181 }
182
183 pub fn push_string(&mut self, other: &Str) {
185 match other {
186 Str::Utf8(s) => self.push_str(s),
187 Str::Wide(w) => {
188 let wide = self.widen();
189 wide.extend(w.iter().copied());
190 }
191 }
192 }
193
194 fn widen(&mut self) -> &mut Vec<u32> {
195 self.wide.get_or_insert_with(|| {
196 let mut wide: Vec<u32> = Vec::with_capacity(self.text.len() + 1);
197 wide.extend(self.text.chars().map(u32::from));
198 self.text = String::new();
199 wide
200 })
201 }
202
203 #[must_use]
204 pub fn is_empty(&self) -> bool {
205 match &self.wide {
206 Some(wide) => wide.is_empty(),
207 None => self.text.is_empty(),
208 }
209 }
210
211 pub fn clear(&mut self) {
216 self.text.clear();
217 self.wide = None;
218 }
219
220 #[must_use]
221 pub fn finish(self) -> Str {
222 match self.wide {
223 Some(wide) => Str::Wide(wide.into_boxed_slice()),
224 None => Str::Utf8(self.text.into_boxed_str()),
225 }
226 }
227}
228
229#[must_use]
234pub fn str_repr(s: &str) -> String {
235 repr_code_points(s.chars().map(u32::from), s.len())
236}
237
238#[must_use]
247pub fn repr_code_points(code_points: impl Iterator<Item = u32> + Clone, hint: usize) -> String {
248 let mut has_single = false;
252 let mut has_double = false;
253 for cp in code_points.clone() {
254 has_single |= cp == u32::from('\'');
255 has_double |= cp == u32::from('"');
256 }
257 let quote = if has_single && !has_double { '"' } else { '\'' };
258
259 let mut out = String::with_capacity(hint + 2);
260 out.push(quote);
261 for cp in code_points {
262 match char::from_u32(cp) {
263 Some('\\') => out.push_str("\\\\"),
264 Some('\t') => out.push_str("\\t"),
265 Some('\n') => out.push_str("\\n"),
266 Some('\r') => out.push_str("\\r"),
267 Some(c) if c == quote => {
268 out.push('\\');
269 out.push(c);
270 }
271 Some(c) if is_printable(c) => out.push(c),
272 _ => push_escape(&mut out, cp),
274 }
275 }
276 out.push(quote);
277 out
278}
279
280#[must_use]
285pub fn bytes_repr(b: &[u8]) -> String {
286 let quote = if b.contains(&b'\'') && !b.contains(&b'"') {
287 b'"'
288 } else {
289 b'\''
290 };
291 let mut out = String::with_capacity(b.len() + 3);
292 out.push('b');
293 out.push(quote as char);
294 for &byte in b {
295 match byte {
296 b'\\' => out.push_str("\\\\"),
297 b'\t' => out.push_str("\\t"),
298 b'\n' => out.push_str("\\n"),
299 b'\r' => out.push_str("\\r"),
300 b if b == quote => {
301 out.push('\\');
302 out.push(b as char);
303 }
304 0x20..=0x7E => out.push(byte as char),
305 b => {
306 let _ = write!(out, "\\x{b:02x}");
307 }
308 }
309 }
310 out.push(quote as char);
311 out
312}
313
314fn push_escape(out: &mut String, cp: u32) {
316 let _ = if cp < 0x100 {
317 write!(out, "\\x{cp:02x}")
318 } else if cp < 0x1_0000 {
319 write!(out, "\\u{cp:04x}")
320 } else {
321 write!(out, "\\U{cp:08x}")
322 };
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn a_string_with_an_apostrophe_changes_quotes_rather_than_escaping() {
331 assert_eq!(str_repr("it's"), "\"it's\"");
332 assert_eq!(str_repr("it's \"so\""), "'it\\'s \"so\"'");
333 assert_eq!(str_repr("\"quoted\""), "'\"quoted\"'");
334 }
335
336 #[test]
337 fn control_characters_are_escaped_and_printable_ones_are_not() {
338 assert_eq!(str_repr("a\tb\nc\rd\\e"), "'a\\tb\\nc\\rd\\\\e'");
339 assert_eq!(str_repr("\x00\x1b\x7f"), "'\\x00\\x1b\\x7f'");
340 assert_eq!(str_repr("héllo"), "'héllo'");
341 assert_eq!(str_repr("\u{200b}"), "'\\u200b'");
342 assert_eq!(str_repr("\u{e0001}"), "'\\U000e0001'");
343 }
344
345 #[test]
346 fn bytes_print_everything_outside_printable_ascii_as_hex() {
347 assert_eq!(bytes_repr(b"abc"), "b'abc'");
348 assert_eq!(bytes_repr(&[0, 0x7f, 0xff]), "b'\\x00\\x7f\\xff'");
349 assert_eq!(bytes_repr(b"it's"), "b\"it's\"");
350 }
351
352 #[test]
355 fn a_lone_surrogate_prints_as_the_escape_that_made_it() {
356 let mut out = StrBuf::new();
357 out.push_code_point(0xD800);
358 assert_eq!(out.finish().repr(), "'\\ud800'");
359 }
360
361 #[test]
363 fn displaying_a_string_writes_the_text_and_not_the_quotes() {
364 assert_eq!(Str::from("it's").to_string(), "it's");
365 assert_eq!(Str::from("a\tb").to_string(), "a\tb");
366
367 let mut out = StrBuf::new();
368 out.push_str("a");
369 out.push_code_point(0xD800);
370 out.push_str("b");
371 assert_eq!(out.finish().to_string(), "a\u{fffd}b");
374 }
375
376 #[test]
380 fn what_looks_like_a_surrogate_pair_stays_two_code_points() {
381 let mut out = StrBuf::new();
382 out.push_code_point(0xD83D);
383 out.push_code_point(0xDE00);
384 let value = out.finish();
385 assert_eq!(value.code_points().count(), 2);
386 assert_eq!(value.repr(), "'\\ud83d\\ude00'");
387 }
388
389 #[test]
392 fn the_quote_choice_survives_widening() {
393 let mut out = StrBuf::new();
394 out.push_str("it's ");
395 out.push_code_point(0xD800);
396 assert_eq!(out.finish().repr(), "\"it's \\ud800\"");
397 }
398
399 #[test]
402 fn widening_keeps_what_was_already_in_the_buffer() {
403 let mut out = StrBuf::new();
404 out.push_str("héllo ");
405 out.push_code_point(0xDFFF);
406 out.push('!');
407 assert_eq!(out.finish().repr(), "'héllo \\udfff!'");
408 }
409
410 #[test]
413 fn the_common_case_never_leaves_the_narrow_arm() {
414 let mut out = StrBuf::new();
415 out.push_str("plain");
416 out.push_code_point(0x1F600);
417 assert!(matches!(out.finish(), Str::Utf8(_)));
418 }
419
420 #[test]
421 fn clearing_a_widened_buffer_goes_back_to_narrow() {
422 let mut out = StrBuf::new();
423 out.push_code_point(0xD800);
424 assert!(!out.is_empty());
425 out.clear();
426 assert!(out.is_empty());
427 out.push_str("after");
428 assert_eq!(out.finish(), Str::Utf8("after".into()));
429 }
430
431 #[test]
432 fn joining_two_strings_widens_only_when_one_of_them_is_wide() {
433 let mut wide = StrBuf::new();
434 wide.push_code_point(0xD800);
435 let wide = wide.finish();
436
437 let mut out = StrBuf::new();
438 out.push_string(&Str::from("a"));
439 out.push_string(&wide);
440 out.push_string(&Str::from("b"));
441 assert_eq!(out.finish().repr(), "'a\\ud800b'");
442 }
443}