gambit_parser/
unescaped.rs1use std::fmt::{self, Display, Formatter};
3use std::iter::{FusedIterator, Peekable};
4use std::str::Chars;
5
6#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8#[repr(transparent)]
9pub struct EscapedStr {
10 escaped: str,
11}
12
13impl EscapedStr {
14 pub(crate) fn new(escaped: &str) -> &Self {
15 debug_assert!(
16 escaped
17 .match_indices('"')
18 .all(|(idx, _)| escaped[..idx].ends_with('\\')),
19 "EscapedStr must not contain an unescaped quote, got {escaped:?}"
20 );
21 unsafe { &*(std::ptr::from_ref::<str>(escaped) as *const EscapedStr) }
24 }
25
26 #[must_use]
28 pub fn escape(&self) -> &str {
29 &self.escaped
30 }
31
32 #[must_use]
34 pub fn unescape(&self) -> Unescaped<'_> {
35 Unescaped {
36 chars: self.escaped.chars().peekable(),
37 }
38 }
39}
40
41impl Display for EscapedStr {
42 fn fmt(&self, out: &mut Formatter<'_>) -> Result<(), fmt::Error> {
43 write!(out, "{}", self.unescape())
44 }
45}
46
47#[derive(Debug, Clone)]
49pub struct Unescaped<'a> {
50 chars: Peekable<Chars<'a>>,
51}
52
53impl Display for Unescaped<'_> {
54 fn fmt(&self, out: &mut Formatter<'_>) -> Result<(), fmt::Error> {
55 for chr in self.clone() {
56 write!(out, "{chr}")?;
57 }
58 Ok(())
59 }
60}
61
62impl Iterator for Unescaped<'_> {
63 type Item = char;
64
65 fn next(&mut self) -> Option<Self::Item> {
66 let chr = self.chars.next()?;
67 if let ('\\', Some(&'"')) = (chr, self.chars.peek()) {
68 self.chars.next()
69 } else {
70 Some(chr)
71 }
72 }
73
74 fn size_hint(&self) -> (usize, Option<usize>) {
75 let (min, max) = self.chars.size_hint();
76 (min.div_ceil(2), max)
77 }
78}
79
80impl FusedIterator for Unescaped<'_> {}
81
82#[cfg(test)]
83mod tests {
84 use super::EscapedStr;
85
86 #[test]
87 fn test_formatting() {
88 let escaped = EscapedStr::new("air \\\" quote");
89 assert_eq!(escaped.to_string(), "air \" quote");
90 }
91
92 #[test]
93 fn unescapes_only_quotes() {
94 assert_eq!(EscapedStr::new(r#"a\"b"#).to_string(), r#"a"b"#);
95 assert_eq!(EscapedStr::new(r"a\b").to_string(), r"a\b");
96 assert_eq!(EscapedStr::new(r"a\nb").to_string(), r"a\nb");
97 assert_eq!(EscapedStr::new(r"a\\b").to_string(), r"a\\b");
98 }
99
100 #[test]
101 fn unescape_collects() {
102 let chars: Vec<char> = EscapedStr::new(r#"a\"b"#).unescape().collect();
104 assert_eq!(chars, ['a', '"', 'b']);
105 }
106}