use proc_macro2::{Delimiter, TokenStream, TokenTree};
use quote::ToTokens;
use std::collections::HashMap;
use syn::{Error, Expr, Pat, Result};
#[derive(Clone)]
pub enum HtmlNode {
Element(HtmlElement),
Text(String),
Variable(Expr),
If(IfNode),
For(ForNode),
}
#[derive(Clone)]
pub struct IfNode {
pub condition: Expr,
pub then_branch: Vec<HtmlNode>,
pub else_branch: Option<Vec<HtmlNode>>,
}
#[derive(Clone)]
pub struct ForNode {
pub pattern: Pat,
pub iterable: Expr,
pub body: Vec<HtmlNode>,
}
#[derive(Clone)]
pub struct HtmlElement {
pub tag: String,
pub attributes: HashMap<String, Option<Expr>>,
pub children: Vec<HtmlNode>,
}
pub fn parse_html(input: TokenStream) -> Result<Vec<HtmlNode>> {
let mut parser = HtmlParser::new(input);
parser.parse_template()
}
struct HtmlParser {
tokens: Vec<TokenTree>,
position: usize,
}
impl HtmlParser {
fn new(input: TokenStream) -> Self {
Self {
tokens: input.into_iter().collect(),
position: 0,
}
}
fn parse_template(&mut self) -> Result<Vec<HtmlNode>> {
let mut nodes = Vec::new();
while !self.is_at_end() {
let token = &self.tokens[self.position];
match token {
TokenTree::Group(group) if group.delimiter() == Delimiter::Brace => {
let node = self.parse_brace_content(group.stream())?;
nodes.push(node);
self.position += 1;
}
TokenTree::Punct(punct) if punct.as_char() == '<' => {
if self.is_closing_tag_start() {
break;
}
let element = self.parse_element()?;
nodes.push(HtmlNode::Element(element));
}
_ => {
let text = self.parse_text_content()?;
if !text.trim().is_empty() {
nodes.push(HtmlNode::Text(text));
}
}
}
}
Ok(nodes)
}
fn parse_text_content(&mut self) -> Result<String> {
let mut text_parts = Vec::new();
while !self.is_at_end() {
let token = &self.tokens[self.position];
match token {
TokenTree::Group(group) if group.delimiter() == Delimiter::Brace => break,
TokenTree::Punct(punct) if punct.as_char() == '<' => break,
_ => {
text_parts.push(token.to_string());
self.position += 1;
}
}
}
Ok(text_parts.join(" "))
}
fn parse_element(&mut self) -> Result<HtmlElement> {
self.expect_punct('<')?;
let tag = self.parse_identifier()?;
let attributes = self.parse_attributes()?;
if self.check_punct('/') {
self.position += 1; self.expect_punct('>')?;
return Ok(HtmlElement {
tag,
attributes,
children: Vec::new(),
});
}
self.expect_punct('>')?;
let children = self.parse_template()?;
self.parse_closing_tag(&tag)?;
Ok(HtmlElement {
tag,
attributes,
children,
})
}
fn parse_attributes(&mut self) -> Result<HashMap<String, Option<Expr>>> {
let mut attributes = HashMap::new();
while !self.is_at_end() && !self.check_punct('>') && !self.check_punct('/') {
let attr_name = self.parse_identifier()?;
if self.check_punct('=') {
self.position += 1;
if let Some(TokenTree::Group(group)) = self.peek() {
if group.delimiter() == Delimiter::Brace {
let expr: Expr = syn::parse2(group.stream())?;
attributes.insert(attr_name, Some(expr));
self.position += 1;
}
} else if let Some(TokenTree::Literal(_)) = self.peek() {
if let Some(TokenTree::Literal(lit)) = self.advance() {
let expr: Expr = syn::parse2(lit.to_token_stream())?;
attributes.insert(attr_name, Some(expr));
}
} else {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected attribute value",
));
}
} else {
attributes.insert(attr_name, None);
}
}
Ok(attributes)
}
fn parse_identifier(&mut self) -> Result<String> {
if let Some(TokenTree::Ident(ident)) = self.advance() {
Ok(ident.to_string())
} else {
Err(Error::new(
proc_macro2::Span::call_site(),
"Expected identifier",
))
}
}
fn is_closing_tag_start(&self) -> bool {
if self.position + 1 < self.tokens.len() {
matches!(
(&self.tokens[self.position], &self.tokens[self.position + 1]),
(TokenTree::Punct(p1), TokenTree::Punct(p2))
if p1.as_char() == '<' && p2.as_char() == '/'
)
} else {
false
}
}
fn parse_closing_tag(&mut self, expected_tag: &str) -> Result<()> {
self.expect_punct('<')?;
self.expect_punct('/')?;
let tag = self.parse_identifier()?;
if tag != expected_tag {
return Err(Error::new(
proc_macro2::Span::call_site(),
format!(
"Expected closing tag for '{}', found '{}'",
expected_tag, tag
),
));
}
self.expect_punct('>')?;
Ok(())
}
fn expect_punct(&mut self, expected: char) -> Result<()> {
if let Some(TokenTree::Punct(punct)) = self.advance() {
if punct.as_char() == expected {
return Ok(());
}
}
Err(Error::new(
proc_macro2::Span::call_site(),
format!("Expected '{}'", expected),
))
}
fn check_punct(&self, expected: char) -> bool {
if let Some(TokenTree::Punct(punct)) = self.peek() {
punct.as_char() == expected
} else {
false
}
}
fn peek(&self) -> Option<&TokenTree> {
self.tokens.get(self.position)
}
fn advance(&mut self) -> Option<&TokenTree> {
if !self.is_at_end() {
let token = &self.tokens[self.position];
self.position += 1;
Some(token)
} else {
None
}
}
fn is_at_end(&self) -> bool {
self.position >= self.tokens.len()
}
fn parse_brace_content(&mut self, content: TokenStream) -> Result<HtmlNode> {
let tokens: Vec<TokenTree> = content.into_iter().collect();
if tokens.is_empty() {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Empty brace content",
));
}
if let Some(TokenTree::Ident(ident)) = tokens.first() {
match ident.to_string().as_str() {
"if" => return self.parse_if_basic(&tokens),
"for" => return self.parse_for_basic(&tokens),
_ => {}
}
}
let expr: Expr = syn::parse2(TokenStream::from_iter(tokens.into_iter()))?;
Ok(HtmlNode::Variable(expr))
}
fn parse_if_basic(&mut self, tokens: &[TokenTree]) -> Result<HtmlNode> {
let mut pos = 1;
let mut condition_tokens = Vec::new();
while pos < tokens.len() {
if let TokenTree::Group(group) = &tokens[pos] {
if group.delimiter() == Delimiter::Brace {
break;
}
}
condition_tokens.push(tokens[pos].clone());
pos += 1;
}
if condition_tokens.is_empty() {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected condition after if",
));
}
let condition: Expr = syn::parse2(TokenStream::from_iter(condition_tokens.into_iter()))?;
if pos >= tokens.len() {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected then block",
));
}
let then_branch = if let TokenTree::Group(group) = &tokens[pos] {
if group.delimiter() == Delimiter::Brace {
let mut parser = HtmlParser::new(group.stream());
parser.parse_template()?
} else {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected brace group",
));
}
} else {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected then block",
));
};
pos += 1;
let else_branch = if pos < tokens.len() {
if let Some(TokenTree::Ident(ident)) = tokens.get(pos) {
if ident == "else" {
pos += 1; if pos >= tokens.len() {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected else block",
));
}
if let TokenTree::Group(group) = &tokens[pos] {
if group.delimiter() == Delimiter::Brace {
let mut parser = HtmlParser::new(group.stream());
Some(parser.parse_template()?)
} else {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected brace group after else",
));
}
} else {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected else block",
));
}
} else {
None
}
} else {
None
}
} else {
None
};
Ok(HtmlNode::If(IfNode {
condition,
then_branch,
else_branch,
}))
}
fn parse_for_basic(&mut self, tokens: &[TokenTree]) -> Result<HtmlNode> {
let mut pos = 1;
let mut pattern_tokens = Vec::new();
while pos < tokens.len() {
if let TokenTree::Ident(ident) = &tokens[pos] {
if ident == "in" {
break;
}
}
pattern_tokens.push(tokens[pos].clone());
pos += 1;
}
if pattern_tokens.is_empty() {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected pattern after for",
));
}
let pattern_stream = TokenStream::from_iter(pattern_tokens);
let pattern: Pat = syn::parse_quote! { #pattern_stream };
if pos >= tokens.len() {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected 'in' keyword",
));
}
pos += 1;
let mut iterable_tokens = Vec::new();
while pos < tokens.len() {
if let TokenTree::Group(group) = &tokens[pos] {
if group.delimiter() == Delimiter::Brace {
break;
}
}
iterable_tokens.push(tokens[pos].clone());
pos += 1;
}
if iterable_tokens.is_empty() {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected iterable expression",
));
}
let iterable: Expr = syn::parse2(TokenStream::from_iter(iterable_tokens.into_iter()))?;
if pos >= tokens.len() {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected body block",
));
}
let body = if let TokenTree::Group(group) = &tokens[pos] {
if group.delimiter() == Delimiter::Brace {
let mut parser = HtmlParser::new(group.stream());
parser.parse_template()?
} else {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected brace group",
));
}
} else {
return Err(Error::new(
proc_macro2::Span::call_site(),
"Expected body block",
));
};
Ok(HtmlNode::For(ForNode {
pattern,
iterable,
body,
}))
}
}