awsl_syn/
node.rs

1//! Tree of nodes
2
3use proc_macro2::{Span, TokenStream};
4use quote::ToTokens;
5use std::fmt;
6use syn::{
7    punctuated::Punctuated, spanned::Spanned, token::Colon, Expr, ExprBlock, ExprPath, Ident, Lit,
8};
9
10use crate::punctuation::Dash;
11
12/// Node in the tree
13#[derive(Debug)]
14pub struct Node {
15    /// Name according to the `NodeType`
16    ///
17    /// - Element: Name of the element
18    /// - Attribute: Key of the element attribute
19    /// - Text: `None`
20    /// - Doctype: `None`
21    /// - Comment: `None`
22    /// - Fragment: `None`
23    /// - Block: `None`
24    pub name: Option<NodeName>,
25
26    /// Type of the node
27    pub node_type: NodeType,
28
29    /// Value according to the `NodeType`
30    ///
31    /// - Element: `None`
32    /// - Attribute: Any valid `syn::Expr`
33    /// - Text: `syn::Expr::Lit`
34    /// - Doctype: `syn::Expr::Lit`
35    /// - Comment: `syn::Expr::Lit`
36    /// - Fragment: `None`
37    /// - Block: `syn::Expr::Block`
38    pub value: Option<Expr>,
39
40    /// `NodeType::Element` attributes are `NodeType::Attribute` or `NodeType::Block`
41    pub attributes: Vec<Node>,
42
43    /// `NodeType::Element` children can be everything except `NodeType::Attribute`
44    pub children: Vec<Node>,
45}
46
47impl Node {
48    /// Returns `String` if `name` is `Some` and not `NodeName::Block`
49    pub fn name_as_string(&self) -> Option<String> {
50        match self.name.as_ref() {
51            Some(NodeName::Block(_)) => None,
52            Some(name) => Some(name.to_string()),
53            None => None,
54        }
55    }
56
57    /// Returns `ExprBlock` if `name` is `NodeName::Block(Expr::Block)`
58    pub fn name_as_block(&self) -> Option<ExprBlock> {
59        match self.name.as_ref() {
60            Some(NodeName::Block(Expr::Block(expr))) => Some(expr.to_owned()),
61            _ => None,
62        }
63    }
64
65    /// Returns `Span` if `name` is `Some`
66    pub fn name_span(&self) -> Option<Span> {
67        self.name.as_ref().map(|name| name.span())
68    }
69
70    /// Returns `String` if `value` is a `Lit` or `Path` expression
71    pub fn value_as_string(&self) -> Option<String> {
72        match self.value.as_ref() {
73            Some(Expr::Lit(expr)) => match &expr.lit {
74                Lit::Str(lit_str) => Some(lit_str.value()),
75                _ => None,
76            },
77            Some(Expr::Path(expr)) => Some(path_to_string(expr)),
78            _ => None,
79        }
80    }
81
82    /// Returns `ExprBlock` if `value` is a `Expr::Block` expression
83    pub fn value_as_block(&self) -> Option<ExprBlock> {
84        match self.value.as_ref() {
85            Some(Expr::Block(expr)) => Some(expr.to_owned()),
86            _ => None,
87        }
88    }
89}
90
91// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
92/// Type of the node
93#[derive(Debug, Clone, PartialEq)]
94pub enum NodeType {
95    /// A HTMLElement tag, with optional children and attributes.
96    /// Potentially selfclosing. Any tag name is valid.
97    Element,
98
99    /// Attributes of opening tags. Every attribute is itself a node.
100    Attribute,
101
102    /// Quoted text. It's [planned to support unquoted text] as well
103    /// using span start and end, but that currently only works
104    /// with nightly rust
105    ///
106    /// [planned to support unquoted text]: https://github.com/stoically/syn-rsx/issues/2
107    Text,
108
109    /// Comment: `<!-- "comment" -->`, currently has the same restrictions as
110    /// `Text` (comment needs to be quoted)
111    Comment,
112
113    /// Doctype declaration: `<!DOCTYPE html>` (case insensitive), `html` is the
114    /// node value in this case
115    Doctype,
116
117    /// Fragment: `<></>`
118    Fragment,
119
120    /// Arbitrary rust code in braced `{}` blocks
121    Block,
122}
123
124impl fmt::Display for NodeType {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        write!(
127            f,
128            "{}",
129            match self {
130                Self::Element => "NodeType::Element",
131                Self::Attribute => "NodeType::Attribute",
132                Self::Text => "NodeType::Text",
133                Self::Comment => "NodeType::Comment",
134                Self::Doctype => "NodeType::Doctype",
135                Self::Fragment => "NodeType::Fragment",
136                Self::Block => "NodeType::Block",
137            }
138        )
139    }
140}
141
142/// Name of the node
143#[derive(Debug)]
144pub enum NodeName {
145    /// A plain identifier like `div` is a path of length 1, e.g. `<div />`. Can
146    /// be separated by double colons, e.g. `<foo::bar />`
147    Path(ExprPath),
148
149    /// Name separated by dashes, e.g. `<div data-foo="bar" />`
150    Dash(Punctuated<Ident, Dash>),
151
152    /// Name separated by colons, e.g. `<div on:click={foo} />`
153    Colon(Punctuated<Ident, Colon>),
154
155    /// Arbitrary rust code in braced `{}` blocks
156    Block(Expr),
157}
158
159impl NodeName {
160    /// Returns the `Span` of this `NodeName`
161    pub fn span(&self) -> Span {
162        match self {
163            NodeName::Path(name) => name.span(),
164            NodeName::Dash(name) => name.span(),
165            NodeName::Colon(name) => name.span(),
166            NodeName::Block(name) => name.span(),
167        }
168    }
169}
170
171impl fmt::Display for NodeName {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        write!(
174            f,
175            "{}",
176            match self {
177                NodeName::Path(expr) => path_to_string(expr),
178                NodeName::Dash(name) => name
179                    .iter()
180                    .map(|ident| ident.to_string())
181                    .collect::<Vec<String>>()
182                    .join("-"),
183                NodeName::Colon(name) => name
184                    .iter()
185                    .map(|ident| ident.to_string())
186                    .collect::<Vec<String>>()
187                    .join(":"),
188                NodeName::Block(_) => String::from("{}"),
189            }
190        )
191    }
192}
193
194impl PartialEq for NodeName {
195    fn eq(&self, other: &NodeName) -> bool {
196        match self {
197            Self::Path(this) => match other {
198                Self::Path(other) => this == other,
199                _ => false,
200            },
201            Self::Dash(this) => match other {
202                Self::Dash(other) => this == other,
203                _ => false,
204            },
205            Self::Colon(this) => match other {
206                Self::Colon(other) => this == other,
207                _ => false,
208            },
209            Self::Block(this) => match other {
210                Self::Block(other) => this == other,
211                _ => false,
212            },
213        }
214    }
215}
216
217impl ToTokens for NodeName {
218    fn to_tokens(&self, tokens: &mut TokenStream) {
219        match self {
220            NodeName::Path(name) => name.to_tokens(tokens),
221            NodeName::Dash(name) => name.to_tokens(tokens),
222            NodeName::Colon(name) => name.to_tokens(tokens),
223            NodeName::Block(name) => name.to_tokens(tokens),
224        }
225    }
226}
227
228fn path_to_string(expr: &ExprPath) -> String {
229    expr.path
230        .segments
231        .iter()
232        .map(|segment| segment.ident.to_string())
233        .collect::<Vec<String>>()
234        .join("::")
235}