Skip to main content

moxy_token/
punct.rs

1use super::ToTokens;
2use super::lex::{Cursor, LexError, Scan};
3use crate::{Spacing, Span, Spanner, TokenStream, TokenTree};
4
5fn spacing_after(text: &str, cursor: Cursor<'_>) -> Spacing {
6    let Some(next) = cursor.first() else {
7        return Spacing::Alone;
8    };
9
10    let current = text.chars().next().unwrap_or_default();
11    let is_compound_pair = matches!(
12        (current, next),
13        ('&', '&' | '=')
14            | ('|', '|' | '=')
15            | ('<', '<' | '=' | '-')
16            | ('>', '>' | '=')
17            | ('=', '=' | '>')
18            | ('!', '=')
19            | ('+', '=')
20            | ('-', '=' | '>')
21            | ('*', '=')
22            | ('/', '=')
23            | ('%', '=')
24            | ('^', '=')
25            | (':', ':')
26            | ('.', '.' | '=')
27    );
28
29    if is_compound_pair || (current == '\'' && (next == '_' || next.is_alphabetic())) {
30        Spacing::Joint
31    } else {
32        Spacing::Alone
33    }
34}
35
36macro_rules! define_punct {
37    ($($name:ident[$is_method:ident, $as_method:ident] => $text:literal),+ $(,)?) => {
38        /// A Rust punctuation token.
39        #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
40        pub enum Punct {
41            $($name($name),)*
42        }
43
44        impl Punct {
45            pub fn as_str(&self) -> &'static str {
46                match self {
47                    $(Self::$name(v) => v.as_str(),)*
48                }
49            }
50
51            pub fn span(&self) -> Span {
52                match self {
53                    $(Self::$name(v) => v.span(),)*
54                }
55            }
56
57            pub fn set_span(&mut self, span: Span) {
58                match self {
59                    $(Self::$name(v) => v.set_span(span),)*
60                }
61            }
62
63            pub fn spacing(&self) -> Spacing {
64                match self {
65                    $(Self::$name(v) => v.spacing(),)*
66                }
67            }
68
69            pub fn set_spacing(&mut self, spacing: Spacing) {
70                match self {
71                    $(Self::$name(v) => v.set_spacing(spacing),)*
72                }
73            }
74
75            #[inline]
76            pub fn to_token_tree(&self) -> TokenTree {
77                TokenTree::Punct(*self)
78            }
79
80            #[inline]
81            pub fn into_token_tree(self) -> TokenTree {
82                TokenTree::Punct(self)
83            }
84        }
85
86        impl ToTokens for Punct {
87            fn to_tokens(&self, tokens: &mut TokenStream) {
88                match self {
89                    $(Self::$name(v) => v.to_tokens(tokens),)*
90                }
91            }
92        }
93
94        impl Spanner for Punct {
95            fn span(&self) -> Span {
96                self.span()
97            }
98        }
99
100        impl std::fmt::Display for Punct {
101            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102                match self {
103                    $(Self::$name(v) => v.fmt(f),)*
104                }
105            }
106        }
107
108        impl Scan for Punct {
109            fn scan(cursor: Cursor<'_>) -> Result<(Cursor<'_>, Self), LexError> {
110                let mut end = cursor;
111                let mut best = None;
112
113                while let Some(ch) = end.first() && ch.is_ascii_punctuation() {
114                    end = end.advance_by(ch.len_utf8());
115                    let text = cursor.slice_to(end);
116
117                    $(
118                        if $name::TEXT == text {
119                            best = Some((end, Self::$name($name {
120                                span: cursor.span_to(&end),
121                                spacing: spacing_after($name::TEXT, end),
122                            })));
123                            continue;
124                        }
125                    )*
126
127                    break;
128                }
129
130                best.ok_or_else(|| cursor.error())
131            }
132        }
133
134        #[cfg(feature = "serde")]
135        impl serde::Serialize for Punct {
136            fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
137            where
138                S: serde::Serializer,
139            {
140                self.as_str().serialize(s)
141            }
142        }
143
144        $(
145            #[doc = concat!("The Rust punctuation token `", $text, "`. ")]
146            #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
147            pub struct $name {
148                span: Span,
149                spacing: Spacing,
150            }
151
152            impl $name {
153                pub const TEXT: &'static str = $text;
154
155                pub fn new(span: Span) -> Self {
156                    Self {
157                        span,
158                        spacing: Spacing::Alone,
159                    }
160                }
161
162                pub fn span(&self) -> Span {
163                    self.span
164                }
165
166                pub fn set_span(&mut self, span: Span) {
167                    self.span = span;
168                }
169
170                pub fn spacing(&self) -> Spacing {
171                    self.spacing
172                }
173
174                pub fn set_spacing(&mut self, spacing: Spacing) {
175                    self.spacing = spacing;
176                }
177
178                pub fn with_spacing(mut self, spacing: Spacing) -> Self {
179                    self.spacing = spacing;
180                    self
181                }
182
183                pub fn as_str(&self) -> &'static str {
184                    Self::TEXT
185                }
186
187                #[inline]
188                pub fn to_token_tree(&self) -> TokenTree {
189                    TokenTree::Punct((*self).into())
190                }
191
192                #[inline]
193                pub fn into_token_tree(self) -> TokenTree {
194                    TokenTree::Punct(self.into())
195                }
196            }
197
198            impl std::fmt::Display for $name {
199                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200                    f.write_str($text)
201                }
202            }
203
204            impl Scan for $name {
205                fn scan(cursor: Cursor<'_>) -> Result<(Cursor<'_>, Self), LexError> {
206                    let end = cursor.advance_by($text.len());
207                    let text = cursor.slice_to(end);
208
209                    if text == $text {
210                        Ok((
211                            end,
212                            Self::new(cursor.span_to(&end)).with_spacing(spacing_after(Self::TEXT, end)),
213                        ))
214                    } else {
215                        cursor.error().into()
216                    }
217                }
218            }
219
220            impl ToTokens for $name {
221                fn to_tokens(&self, tokens: &mut TokenStream) {
222                    tokens.extend_one(TokenTree::Punct(Punct::$name(*self)));
223                }
224            }
225
226            impl Spanner for $name {
227                fn span(&self) -> Span {
228                    self.span
229                }
230            }
231
232            impl From<$name> for Punct {
233                fn from(value: $name) -> Self {
234                    Self::$name(value)
235                }
236            }
237
238            #[cfg(feature = "serde")]
239            impl serde::Serialize for $name {
240                fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
241                where
242                    S: serde::Serializer,
243                {
244                    self.as_str().serialize(s)
245                }
246            }
247        )+
248
249        impl TokenTree {
250            pub fn is_punct(&self) -> bool {
251                matches!(self, Self::Punct(_))
252            }
253
254            pub fn as_punct(&self) -> Option<&Punct> {
255                match self {
256                    Self::Punct(v) => Some(v),
257                    _ => None,
258                }
259            }
260
261            $(
262                #[doc = concat!("**", stringify!($name), "** (\"", $text, "\")")]
263                pub fn $is_method(&self) -> bool {
264                    matches!(self, Self::Punct(Punct::$name(_)))
265                }
266
267                #[doc = concat!("**", stringify!($name), "** (\"", $text, "\")")]
268                pub fn $as_method(&self) -> Option<&$name> {
269                    match self {
270                        Self::Punct(Punct::$name(v)) => Some(v),
271                        _ => None,
272                    }
273                }
274            )*
275        }
276    };
277
278}
279
280define_punct! {
281    And[is_punct_and, as_punct_and]                         => "&",
282    Or[is_punct_or, as_punct_or]                            => "|",
283    Not[is_punct_not, as_punct_not]                         => "!",
284    Tilde[is_punct_tilde, as_punct_tilde]                   => "~",
285    Plus[is_punct_plus, as_punct_plus]                      => "+",
286    Minus[is_punct_minus, as_punct_minus]                   => "-",
287    Underscore[is_punct_underscore, as_punct_underscore]    => "_",
288    Star[is_punct_star, as_punct_star]                      => "*",
289    Slash[is_punct_slash, as_punct_slash]                   => "/",
290    Percent[is_punct_percent, as_punct_percent]             => "%",
291    Caret[is_punct_caret, as_punct_caret]                   => "^",
292    Eq[is_punct_eq, as_punct_eq]                            => "=",
293    Lt[is_punct_lt, as_punct_lt]                            => "<",
294    Gt[is_punct_gt, as_punct_gt]                            => ">",
295    At[is_punct_at, as_punct_at]                            => "@",
296    Dot[is_punct_dot, as_punct_dot]                         => ".",
297    Comma[is_punct_comma, as_punct_comma]                   => ",",
298    Semi[is_punct_semi, as_punct_semi]                      => ";",
299    Colon[is_punct_colon, as_punct_colon]                   => ":",
300    Pound[is_punct_pound, as_punct_pound]                   => "#",
301    Dollar[is_punct_dollar, as_punct_dollar]                => "$",
302    Question[is_punct_question, as_punct_question]          => "?",
303    Quote[is_punct_quote, as_punct_quote]                   => "'",
304}