use syn::{
parse::{Parse, ParseStream},
token, Expr, Ident, Result, Token,
};
#[derive(Debug)]
pub struct RsxElement {
pub name: Ident,
pub attributes: Vec<RsxAttribute>,
pub children: Vec<RsxNode>,
#[allow(dead_code)]
pub self_closing: bool,
}
#[derive(Debug)]
pub enum RsxAttribute {
Flag(Ident),
Value { name: Ident, value: Expr },
}
#[derive(Debug)]
pub enum RsxNode {
Element(RsxElement),
Expr(Expr),
}
impl Parse for RsxElement {
fn parse(input: ParseStream) -> Result<Self> {
input.parse::<Token![<]>()?;
let name: Ident = input.parse()?;
let mut attributes = Vec::new();
while !input.peek(Token![>]) && !input.peek(Token![/]) {
let attr_name: Ident = input.parse()?;
if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = if input.peek(token::Brace) {
let content;
syn::braced!(content in input);
content.parse()?
} else {
let lit: syn::Lit = input.parse()?;
syn::Expr::Lit(syn::ExprLit {
attrs: vec![],
lit,
})
};
attributes.push(RsxAttribute::Value {
name: attr_name,
value,
});
} else {
attributes.push(RsxAttribute::Flag(attr_name));
}
}
let self_closing = if input.peek(Token![/]) {
input.parse::<Token![/]>()?;
input.parse::<Token![>]>()?;
true
} else {
input.parse::<Token![>]>()?;
false
};
let mut children = Vec::new();
if !self_closing {
loop {
if input.peek(Token![<]) && input.peek2(Token![/]) {
break;
}
if input.is_empty() {
return Err(syn::Error::new(
input.span(),
format!("unclosed tag '{name}'"),
));
}
if input.peek(token::Brace) {
let content;
syn::braced!(content in input);
let expr: Expr = content.parse()?;
children.push(RsxNode::Expr(expr));
} else if input.peek(Token![<]) {
children.push(RsxNode::Element(input.parse()?));
} else {
return Err(syn::Error::new(
input.span(),
format!("unexpected token in <{name}>: expected '{{expr}}', '<child>' or '</{name}>'"),
));
}
}
input.parse::<Token![<]>()?;
input.parse::<Token![/]>()?;
let closing_name: Ident = input.parse()?;
input.parse::<Token![>]>()?;
if name != closing_name {
return Err(syn::Error::new_spanned(
&closing_name,
format!("closing tag '{closing_name}' does not match opening tag '{name}'"),
));
}
}
Ok(RsxElement {
name,
attributes,
children,
self_closing,
})
}
}