rstml/node/
node_name.rs

1use std::{
2    convert::TryFrom,
3    fmt::{self, Display},
4};
5
6use proc_macro2::Punct;
7use syn::{
8    ext::IdentExt,
9    parse::{discouraged::Speculative, Parse, ParseStream, Peek},
10    punctuated::{Pair, Punctuated},
11    token::{Brace, Colon, Dot, PathSep},
12    Block, ExprPath, Ident, LitInt, Path, PathSegment,
13};
14
15use super::{atoms::tokens::Dash, path_to_string};
16use crate::{node::parse::block_expr, Error};
17
18#[derive(Clone, Debug, syn_derive::Parse, syn_derive::ToTokens)]
19pub enum NodeNameFragment {
20    #[parse(peek = Ident::peek_any)]
21    Ident(#[parse(Ident::parse_any)] Ident),
22    #[parse(peek = LitInt)]
23    Literal(LitInt),
24    // In case when name contain more than one Punct in series
25    Empty,
26}
27
28impl PartialEq<NodeNameFragment> for NodeNameFragment {
29    fn eq(&self, other: &NodeNameFragment) -> bool {
30        match (self, other) {
31            (NodeNameFragment::Ident(s), NodeNameFragment::Ident(o)) => s == o,
32            // compare literals by their string representation
33            // So 0x00 and 0 is would be different literals.
34            (NodeNameFragment::Literal(s), NodeNameFragment::Literal(o)) => {
35                s.to_string() == o.to_string()
36            }
37            (NodeNameFragment::Empty, NodeNameFragment::Empty) => true,
38            _ => false,
39        }
40    }
41}
42impl Eq for NodeNameFragment {}
43
44impl Display for NodeNameFragment {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            NodeNameFragment::Ident(i) => i.fmt(f),
48            NodeNameFragment::Literal(l) => l.fmt(f),
49            NodeNameFragment::Empty => Ok(()),
50        }
51    }
52}
53
54/// Name of the node.
55#[derive(Clone, Debug, syn_derive::ToTokens)]
56pub enum NodeName {
57    /// A plain identifier like `div` is a path of length 1, e.g. `<div />`. Can
58    /// be separated by double colons, e.g. `<foo::bar />`.
59    Path(ExprPath),
60
61    ///
62    /// Name separated by punctuation, e.g. `<div data-foo="bar" />` or `<div
63    /// data:foo="bar" />`.
64    ///
65    /// It is fully compatible with SGML (ID/NAME) tokens format.
66    /// Which is described as follow:
67    /// ID and NAME tokens must begin with a letter ([A-Za-z]) and may be
68    /// followed by any number of letters, digits ([0-9]), hyphens ("-"),
69    /// underscores ("_"), colons (":"), and periods (".").
70    ///
71    /// Support more than one punctuation in series, in this case
72    /// `NodeNameFragment::Empty` would be used.
73    ///
74    /// Note: that punct and `NodeNameFragment` has different `Spans` and IDE
75    /// (rust-analyzer/idea) can controll them independently.
76    /// So if one needs to add semantic highlight or go-to definition to entire
77    /// `NodeName` it should emit helper statements for each `Punct` and
78    /// `NodeNameFragment` (excludeing `Empty` fragment).
79    Punctuated(Punctuated<NodeNameFragment, Punct>),
80
81    /// Arbitrary rust code in braced `{}` blocks.
82    Block(Block),
83}
84
85impl NodeName {
86    /// Returns true if `NodeName` parsed as block of code.
87    ///
88    /// Example:
89    /// {"Foo"}
90    pub fn is_block(&self) -> bool {
91        matches!(self, Self::Block(_))
92    }
93
94    /// Returns true if `NodeName` is dash seperated.
95    ///
96    /// Example:
97    /// foo-bar
98    pub fn is_dashed(&self) -> bool {
99        match self {
100            Self::Punctuated(p) => {
101                let p = p.pairs().next().unwrap();
102                p.punct().unwrap().as_char() == '-'
103            }
104            _ => false,
105        }
106    }
107
108    /// Returns true if `NodeName` is wildcard ident.
109    ///
110    /// Example:
111    /// _
112    pub fn is_wildcard(&self) -> bool {
113        match self {
114            Self::Path(e) => {
115                if e.path.segments.len() != 1 {
116                    return false;
117                }
118                let Some(last_ident) = e.path.segments.last() else {
119                    return false;
120                };
121                last_ident.ident == "_"
122            }
123            _ => false,
124        }
125    }
126
127    /// Parse the stream as punctuated idents.
128    ///
129    /// We can't replace this with [`Punctuated::parse_separated_nonempty`]
130    /// since that doesn't support reserved keywords. Might be worth to
131    /// consider a PR upstream.
132    ///
133    /// [`Punctuated::parse_separated_nonempty`]: https://docs.rs/syn/1.0.58/syn/punctuated/struct.Punctuated.html#method.parse_separated_nonempty
134    pub(crate) fn node_name_punctuated_ident<T: Parse, F: Peek, X: From<Ident>>(
135        input: ParseStream,
136        punct: F,
137    ) -> syn::Result<Punctuated<X, T>> {
138        let fork = &input.fork();
139        let mut segments = Punctuated::<X, T>::new();
140
141        while !fork.is_empty() && fork.peek(Ident::peek_any) {
142            let ident = Ident::parse_any(fork)?;
143            segments.push_value(ident.clone().into());
144
145            if fork.peek(punct) {
146                segments.push_punct(fork.parse()?);
147            } else {
148                break;
149            }
150        }
151
152        if segments.len() > 1 {
153            input.advance_to(fork);
154            Ok(segments)
155        } else {
156            Err(fork.error("expected punctuated node name"))
157        }
158    }
159
160    /// Parse the stream as punctuated idents, with two possible punctuations
161    /// available
162    pub(crate) fn node_name_punctuated_ident_with_two_alternate<
163        T: Parse,
164        F: Peek,
165        G: Peek,
166        H: Peek,
167        X: From<NodeNameFragment>,
168    >(
169        input: ParseStream,
170        punct: F,
171        alternate_punct: G,
172        alternate_punct2: H,
173    ) -> syn::Result<Punctuated<X, T>> {
174        let fork = &input.fork();
175        let mut segments = Punctuated::<X, T>::new();
176
177        while !fork.is_empty() {
178            let ident = NodeNameFragment::parse(fork)?;
179            segments.push_value(ident.clone().into());
180
181            if fork.peek(punct) || fork.peek(alternate_punct) || fork.peek(alternate_punct2) {
182                segments.push_punct(fork.parse()?);
183            } else {
184                break;
185            }
186        }
187
188        if segments.len() > 1 {
189            input.advance_to(fork);
190            Ok(segments)
191        } else {
192            Err(fork.error("expected punctuated node name"))
193        }
194    }
195}
196
197impl TryFrom<&NodeName> for Block {
198    type Error = Error;
199
200    fn try_from(node: &NodeName) -> Result<Self, Self::Error> {
201        match node {
202            NodeName::Block(b) => Ok(b.to_owned()),
203            _ => Err(Error::TryFrom(
204                "NodeName does not match NodeName::Block(Expr::Block(_))".into(),
205            )),
206        }
207    }
208}
209
210impl PartialEq for NodeName {
211    fn eq(&self, other: &NodeName) -> bool {
212        match self {
213            Self::Path(this) => match other {
214                Self::Path(other) => this == other,
215                _ => false,
216            },
217            // can't be derived automatically because `Punct` doesn't impl `PartialEq`
218            Self::Punctuated(this) => match other {
219                Self::Punctuated(other) => {
220                    this.pairs()
221                        .zip(other.pairs())
222                        .all(|(this, other)| match (this, other) {
223                            (
224                                Pair::Punctuated(this_ident, this_punct),
225                                Pair::Punctuated(other_ident, other_punct),
226                            ) => {
227                                this_ident == other_ident
228                                    && this_punct.as_char() == other_punct.as_char()
229                            }
230                            (Pair::End(this), Pair::End(other)) => this == other,
231                            _ => false,
232                        })
233                }
234                _ => false,
235            },
236            Self::Block(this) => match other {
237                Self::Block(other) => this == other,
238                _ => false,
239            },
240        }
241    }
242}
243
244impl fmt::Display for NodeName {
245    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246        write!(
247            f,
248            "{}",
249            match self {
250                NodeName::Path(expr) => path_to_string(expr),
251                NodeName::Punctuated(name) => {
252                    name.pairs()
253                        .flat_map(|pair| match pair {
254                            Pair::Punctuated(ident, punct) => {
255                                [ident.to_string(), punct.to_string()]
256                            }
257                            Pair::End(ident) => [ident.to_string(), "".to_string()],
258                        })
259                        .collect::<String>()
260                }
261                NodeName::Block(_) => String::from("{}"),
262            }
263        )
264    }
265}
266
267impl Parse for NodeName {
268    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
269        if input.peek(LitInt) {
270            Err(syn::Error::new(
271                input.span(),
272                "Name must start with latin character",
273            ))
274        } else if input.peek2(PathSep) {
275            NodeName::node_name_punctuated_ident::<PathSep, fn(_) -> PathSep, PathSegment>(
276                input, PathSep,
277            )
278            .map(|segments| {
279                NodeName::Path(ExprPath {
280                    attrs: vec![],
281                    qself: None,
282                    path: Path {
283                        leading_colon: None,
284                        segments,
285                    },
286                })
287            })
288        } else if input.peek2(Colon) || input.peek2(Dash) || input.peek2(Dot) {
289            NodeName::node_name_punctuated_ident_with_two_alternate::<
290                Punct,
291                fn(_) -> Colon,
292                fn(_) -> Dash,
293                fn(_) -> Dot,
294                NodeNameFragment,
295            >(input, Colon, Dash, Dot)
296            .map(NodeName::Punctuated)
297        } else if input.peek(Brace) {
298            let fork = &input.fork();
299            let value = block_expr(fork)?;
300            input.advance_to(fork);
301            Ok(NodeName::Block(value))
302        } else if input.peek(Ident::peek_any) {
303            let mut segments = Punctuated::new();
304            let ident = Ident::parse_any(input)?;
305            segments.push_value(PathSegment::from(ident));
306            Ok(NodeName::Path(ExprPath {
307                attrs: vec![],
308                qself: None,
309                path: Path {
310                    leading_colon: None,
311                    segments,
312                },
313            }))
314        } else {
315            Err(input.error("invalid tag name or attribute key"))
316        }
317    }
318}