Skip to main content

onepass_seed/expr/
repr.rs

1use core::{
2    fmt::{self, Result, Write},
3    mem,
4};
5
6use super::{
7    Context, Expr, Node,
8    chars::{CharRange, Chars, next_char},
9};
10
11struct ReprState<'a, 'b>(bool, &'a Context<'b>);
12
13impl Expr<'_> {
14    /// Write the canonical serialization of this expression. This function implements this type’s
15    /// [`fmt::Display`].
16    pub fn write_repr<W>(&self, w: &mut W) -> Result
17    where
18        W: Write,
19    {
20        ReprState(false, self.get_context()).write(w, &self.root)
21    }
22}
23
24impl Chars {
25    pub fn write_repr<W>(&self, w: &mut W) -> Result
26    where
27        W: Write,
28    {
29        write!(w, "[")?;
30        if let Some(hyphen) = self.0.iter().find(|cr| cr.start == '-') {
31            fmt_charclass(w, hyphen)?;
32        }
33        self.0
34            .iter()
35            .filter(|&cr| cr.start != '-' && cr.end != '-')
36            .try_fold((), |(), cr| fmt_charclass(w, cr))?;
37        if let Some(hyphen) = self.0.iter().find(|cr| cr.end == '-' && cr.start != '-') {
38            fmt_charclass(w, hyphen)?;
39        }
40        write!(w, "]")?;
41        Ok(())
42    }
43}
44
45impl ReprState<'_, '_> {
46    pub fn write<W>(&mut self, w: &mut W, node: &Node) -> Result
47    where
48        W: Write,
49    {
50        match *node {
51            Node::Literal(ref s) => write_literal(w, s),
52            Node::Chars(ref chars) => write!(w, "{chars}"),
53            Node::List(ref list) => {
54                let nested = mem::replace(&mut self.0, true);
55                if nested {
56                    write!(w, "(")?;
57                }
58                list.iter().try_for_each(|node| self.write(w, node))?;
59                if nested {
60                    write!(w, ")")?;
61                }
62                Ok(())
63            }
64
65            Node::Count(ref node, min, max) => {
66                self.0 = true;
67                self.write(w, node)?;
68                w.write_char('{')?;
69                // NB. it is legal to have max == 0.
70                if min != 0 || max == 0 {
71                    write!(w, "{min}")?;
72                }
73                if max != min {
74                    write!(w, ",{max}")?;
75                }
76                w.write_char('}')
77            }
78
79            Node::Generator(ref generator) => {
80                w.write_char('{')?;
81                self.1.get_generator(generator.name()).unwrap().write_repr(
82                    self.1,
83                    w,
84                    &generator.args(),
85                )?;
86                w.write_char('}')?;
87                Ok(())
88            }
89        }
90    }
91}
92
93impl fmt::Display for Expr<'_> {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result {
95        self.write_repr(f)
96    }
97}
98
99impl fmt::Display for Chars {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result {
101        self.write_repr(f)
102    }
103}
104
105pub enum Escape {
106    Hex,
107    Str(&'static str),
108}
109
110pub fn write_literal<W>(w: &mut W, s: &str) -> Result
111where
112    W: fmt::Write + ?Sized,
113{
114    use Escape::*;
115    let mut pos = 0;
116    for (i, b) in s.bytes().enumerate() {
117        let escaped = match b {
118            b'\\' => Str("\\\\"),
119            b'(' => Str("\\("),
120            b')' => Str("\\)"),
121            b'[' => Str("\\["),
122            b']' => Str("\\]"),
123            b'{' => Str("\\{"),
124            b'}' => Str("\\}"),
125            b'|' => Str("\\|"),
126            b'\x00'..b'\x20' | b'\x7f' => Hex,
127            _ => continue,
128        };
129        if pos != i {
130            w.write_str(&s[pos..i])?;
131        }
132        match escaped {
133            Str(s) => w.write_str(s),
134            Hex => write!(w, "\\x{b:02x}"),
135        }?;
136        pos = i + 1;
137    }
138    if pos != s.len() {
139        w.write_str(&s[pos..])?;
140    }
141    Ok(())
142}
143
144pub fn write_escape<W>(w: &mut W, c: char) -> Result
145where
146    W: Write,
147{
148    match c {
149        '\x00'..'\x20' | '\x7f' => write!(w, "\\x{:02x}", c as u8),
150        ']' => w.write_str("\\]"),
151        '\\' => w.write_str("\\\\"),
152        c if c.is_ascii() => w.write_char(c),
153        _ => write!(w, "{}", c.escape_debug()),
154    }
155}
156
157pub fn fmt_charclass<W>(w: &mut W, cr: &CharRange) -> Result
158where
159    W: Write,
160{
161    write_escape(w, cr.start)?;
162    if cr.end != cr.start {
163        if let Some(next) = next_char(cr.start)
164            && next != cr.end
165        {
166            w.write_char('-')?;
167        }
168        write_escape(w, cr.end)?;
169    }
170    Ok(())
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_chars_hyphens() {
179        let tests: [(&str, &[(char, char)]); _] = [
180            ("[-a]", &[('-', '-'), ('a', 'a')]),
181            ("[Z!--]", &[('Z', 'Z'), ('!', '-')]),
182            ("[\\\\\\]]", &[('\\', ']')]),
183            ("[!-#]", &[('!', '#')]),
184            ("[!\"]", &[('!', '"')]),
185        ];
186        for (want, cs) in tests {
187            let cs = Chars::from_ranges(cs.iter().copied());
188            eprintln!("want=\"{want}\" cs={cs:?}");
189            assert_eq!(want, &format!("{cs}"));
190            let expr = Expr::new(want.parse().unwrap());
191            assert_eq!(want, &format!("{expr}"), "{want:?} cs={cs:?}");
192        }
193    }
194
195    #[test]
196    fn test_non_printable() {
197        for (want, root) in [
198            (
199                "[\\x00-\\x7f]",
200                Node::Chars(Chars::from_ranges([('\0', '\x7f')])),
201            ),
202            (
203                "[\u{2014}-\u{2026}]",
204                Node::Chars(Chars::from_ranges([('—', '…')])),
205            ),
206            (r#"\x00—\x7f"#, Node::Literal("\0—\x7f".into())),
207        ] {
208            assert_eq!(want, &format!("{}", Expr::new(root.clone())));
209            assert_eq!(root, want.parse().unwrap());
210        }
211    }
212
213    #[test]
214    fn test_literal() {
215        assert_eq!(
216            r#"\{\}"#,
217            &format!("{}", Expr::new(Node::Literal("{}".into())))
218        );
219    }
220
221    #[test]
222    fn test_nested() {
223        assert_eq!(
224            "([a-z][0-9]){3,6}",
225            &format!("{}", Expr::parse("([a-z][0-9]){3,6}").unwrap())
226        );
227        assert_eq!(
228            "[a-z]{,3}",
229            &format!("{}", Expr::parse("[a-z]{0,3}").unwrap())
230        );
231    }
232}