rsexpr/
display.rs

1use std::fmt::{self, Display, Formatter, Write as _};
2
3use crate::{OwnedSexpr, OwnedSexprs, Sexpr, Sexprs};
4
5impl Display for OwnedSexpr {
6    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
7        Sexpr::from(self).fmt(f)
8    }
9}
10
11impl Display for OwnedSexprs {
12    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
13        Sexprs::from(self).fmt(f)
14    }
15}
16
17impl Display for Sexprs<'_> {
18    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
19        let last = self.len().saturating_sub(1);
20        let mut iter = self.iter().enumerate().peekable();
21        while let Some((index, sexpr)) = iter.next() {
22            sexpr.fmt(f)?;
23
24            if f.alternate() && index != last {
25                match sexpr {
26                    #[cfg(feature = "comments")]
27                    Sexpr::Comment(_) => writeln!(f)?,
28                    _ if matches!(iter.peek(), Some((_, Sexpr::Atom(_)))) => write!(f, " ")?,
29                    _ => write!(f, "\n\n")?,
30                }
31            }
32        }
33
34        if f.alternate() {
35            writeln!(f)?;
36        }
37
38        Ok(())
39    }
40}
41
42impl Display for Sexpr<'_> {
43    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
44        let pretty = f.alternate();
45        let mut indent_level = f.width().unwrap_or(0);
46        let indent_width = f.precision().unwrap_or(2);
47        let indent = |level| " ".repeat(indent_width * level);
48
49        if !pretty {
50            match self {
51                Sexpr::List(children) | Sexpr::Group(children) => {
52                    let (open_paren, close_paren) = match self {
53                        Sexpr::List(_) => ('(', ')'),
54                        _ => ('[', ']'),
55                    };
56
57                    let children = children.iter().fold(String::new(), |mut out, child| {
58                        _ = write!(out, "{child}");
59                        out
60                    });
61
62                    write!(
63                        f,
64                        "{open_paren}{}{close_paren}",
65                        // trim possible whitespace at end
66                        children.trim_end(),
67                    )?;
68                }
69                Sexpr::String(string) => write!(
70                    f,
71                    "\"{}\" ",
72                    String::from_utf8_lossy(string)
73                        .replace('\\', r"\\")
74                        .replace('"', "\\\"")
75                )?,
76                Sexpr::Atom(atom) => write!(f, "{} ", String::from_utf8_lossy(atom))?,
77                #[cfg(feature = "comments")]
78                Sexpr::Comment(_) => {}
79            }
80        } else {
81            match self {
82                Sexpr::List(children) | Sexpr::Group(children) => {
83                    let (open_paren, close_paren) = match self {
84                        Sexpr::List(_) => ('(', ')'),
85                        _ => ('[', ']'),
86                    };
87
88                    // keep lists in one line if they start with a predicate, unless they contain a
89                    // comment or have more than 7 children
90                    if let (Some(Self::Atom([b'#', ..])), Sexpr::List(_)) = (children.first(), self)
91                    {
92                        #[cfg(feature = "comments")]
93                        let has_comment_child = children
94                            .iter()
95                            .any(|child| matches!(child, Sexpr::Comment(_)));
96                        #[cfg(not(feature = "comments"))]
97                        let has_comment_child = false;
98
99                        if !has_comment_child && children.len() <= 7 {
100                            // call doesn't cause infinite recursion,
101                            // because `f.alternate()` is different
102                            #[allow(clippy::recursive_format_impl)]
103                            return write!(f, "{self}");
104                        }
105                    }
106
107                    write!(f, "{open_paren}")?;
108                    match children.len() {
109                        0 => {}
110                        1 => match &children[0] {
111                            child @ (Sexpr::List(list) | Sexpr::Group(list)) if list.is_empty() => {
112                                write!(f, "{child:#}")?
113                            }
114                            child @ (Sexpr::List(_) | Sexpr::Group(_)) => {
115                                indent_level += 1;
116                                write!(f, "\n{}", indent(indent_level))?;
117                                write!(f, "{child:#indent_level$.indent_width$}")?;
118                                indent_level -= 1;
119                                write!(f, "\n{}", indent(indent_level))?;
120                            }
121                            child @ (Sexpr::String(_) | Sexpr::Atom(_)) => write!(f, "{child:#}")?,
122                            #[cfg(feature = "comments")]
123                            child @ Sexpr::Comment(_) => {
124                                indent_level += 1;
125                                write!(f, "\n{}", indent(indent_level))?;
126                                write!(f, "{child:#indent_level$.indent_width$}")?;
127                                indent_level -= 1;
128                                write!(f, "\n{}", indent(indent_level))?;
129                            }
130                        },
131                        _ => {
132                            indent_level += 1;
133                            let newline = format!("\n{}", indent(indent_level));
134                            let mut iter = children.windows(2).peekable();
135
136                            // if the first child is a string or atom, keep it on the same line
137                            let mut second_child = None;
138                            if let (
139                                Some([Sexpr::String(_) | Sexpr::Atom(_), second]),
140                                Sexpr::List(_),
141                            ) = (iter.peek(), self)
142                            {
143                                second_child = Some(second);
144                                let child = &iter.next().unwrap()[0];
145                                write!(f, "{child:#}")?;
146                            }
147
148                            write!(f, "{newline}")?;
149
150                            // write the first child
151                            if let (Some([child, _]), _) | (_, Some(child)) =
152                                (iter.peek(), second_child)
153                            {
154                                write!(f, "{child:#indent_level$.indent_width$}")?;
155                            }
156
157                            // write all other children
158                            for item in iter {
159                                let prev_child = &item[0];
160                                let child = &item[1];
161                                match (prev_child, child) {
162                                    // if the current child is a quantifier, add it in the same
163                                    // line without a leading whitespace
164                                    (_, Sexpr::Atom([char @ (b'?' | b'*' | b'+')])) => {
165                                        write!(f, "{}", *char as char)?;
166                                    }
167                                    // if the previous child was an atom ending with `:` or the
168                                    // current child is and atom starting with `@`, stay on the
169                                    // same line
170                                    (
171                                        Sexpr::Atom([.., b':']),
172                                        child @ (Sexpr::List(_) | Self::Group(_)),
173                                    )
174                                    | (_, child @ Sexpr::Atom([b'@', ..])) => {
175                                        write!(f, " {child:#indent_level$.indent_width$}")?;
176                                    }
177                                    // else go to the next line
178                                    (_, child) => {
179                                        write!(f, "{newline}{child:#indent_level$.indent_width$}")?;
180                                    }
181                                }
182                            }
183
184                            indent_level -= 1;
185                            write!(f, "\n{}", indent(indent_level))?;
186                        }
187                    }
188                    write!(f, "{close_paren}")?;
189                }
190                Sexpr::String(string) => write!(
191                    f,
192                    "\"{}\"",
193                    String::from_utf8_lossy(string)
194                        .replace('\\', r"\\")
195                        .replace('"', "\\\"")
196                )?,
197                Sexpr::Atom(atom) => write!(f, "{}", String::from_utf8_lossy(atom))?,
198                #[cfg(feature = "comments")]
199                Sexpr::Comment(comment) => write!(f, "{}", String::from_utf8_lossy(comment))?,
200            }
201        }
202
203        Ok(())
204    }
205}