Skip to main content

gambit_parser/
unescaped.rs

1//! A wrapper for strings that have escape characters in them
2use std::fmt::{self, Display, Formatter};
3use std::iter::{FusedIterator, Peekable};
4use std::str::Chars;
5
6/// A string with backslash escapes in it
7#[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        // SAFETY: `EscapedStr` is `#[repr(transparent)]` over `str`, so a `&str` and a
22        // `&EscapedStr` share the same layout.
23        unsafe { &*(std::ptr::from_ref::<str>(escaped) as *const EscapedStr) }
24    }
25
26    /// The string in its original, escaped form
27    #[must_use]
28    pub fn escape(&self) -> &str {
29        &self.escaped
30    }
31
32    /// Get an iterator over the true characters
33    #[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/// An iterator over the true characters of an [`EscapedStr`]
48#[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        // collecting drives size_hint; `\"` is the one sequence that shrinks two chars to one
103        let chars: Vec<char> = EscapedStr::new(r#"a\"b"#).unescape().collect();
104        assert_eq!(chars, ['a', '"', 'b']);
105    }
106}