use proc_macro2::{Delimiter, Group, Literal, TokenStream, TokenTree};
use crate::ast::{
AttributeTemplate, AttributeValue, ComponentTemplate, ElementTemplate, RustExpr, RustPath,
Template, TemplateNode, XmlName,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ParseError {
message: String,
}
impl ParseError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
pub(crate) fn from_message(message: impl Into<String>) -> Self {
Self::new(message)
}
pub(crate) fn message(&self) -> &str {
&self.message
}
}
type ParseResult<T> = Result<T, ParseError>;
pub(crate) fn parse_template(input: TokenStream) -> ParseResult<Template> {
let mut parser = Parser::new(input);
let nodes = parser.parse_nodes_until(None)?;
parser.expect_eof()?;
if nodes.is_empty() {
return Err(ParseError::new("xml! template cannot be empty"));
}
Ok(Template { nodes })
}
struct Parser {
tokens: Vec<TokenTree>,
position: usize,
}
impl Parser {
fn new(input: TokenStream) -> Self {
Self {
tokens: input.into_iter().collect(),
position: 0,
}
}
fn parse_nodes_until(&mut self, closing: Option<&TagName>) -> ParseResult<Vec<TemplateNode>> {
let mut nodes = Vec::new();
while !self.is_eof() {
if self.peek_punct('<') {
if self.peek_punct_at(1, '/') {
let found = self.parse_closing_tag()?;
return match closing {
Some(expected) if &found == expected => Ok(nodes),
Some(expected) => Err(ParseError::new(format!(
"mismatched closing tag: expected </{}>, found </{}>",
expected.lexical(),
found.lexical()
))),
None => Err(ParseError::new(format!(
"unexpected closing tag </{}>",
found.lexical()
))),
};
}
if self.starts_comment() {
nodes.push(TemplateNode::Comment(self.parse_comment()?));
continue;
}
nodes.push(self.parse_tag()?);
continue;
}
if let Some(group) = self.peek_brace_group() {
let tokens = group.stream();
self.position += 1;
nodes.push(TemplateNode::Expr(RustExpr { tokens }));
continue;
}
nodes.push(TemplateNode::Text(self.parse_text()?));
}
match closing {
Some(expected) => Err(ParseError::new(format!(
"unclosed tag <{}>",
expected.lexical()
))),
None => Ok(nodes),
}
}
fn parse_tag(&mut self) -> ParseResult<TemplateNode> {
self.expect_punct('<')?;
let tag_name = self.parse_tag_name()?;
let attributes = self.parse_attributes()?;
if self.consume_punct('/') {
self.expect_punct('>')?;
return Ok(match tag_name {
TagName::Element(name) => TemplateNode::Element(ElementTemplate {
name,
attributes,
children: Vec::new(),
self_closing: true,
}),
TagName::Component(path) => TemplateNode::Component(ComponentTemplate {
path,
props: attributes,
children: Vec::new(),
self_closing: true,
}),
});
}
self.expect_punct('>')?;
let children = self.parse_nodes_until(Some(&tag_name))?;
Ok(match tag_name {
TagName::Element(name) => TemplateNode::Element(ElementTemplate {
name,
attributes,
children,
self_closing: false,
}),
TagName::Component(path) => TemplateNode::Component(ComponentTemplate {
path,
props: attributes,
children,
self_closing: false,
}),
})
}
fn parse_closing_tag(&mut self) -> ParseResult<TagName> {
self.expect_punct('<')?;
self.expect_punct('/')?;
let name = self.parse_tag_name()?;
self.expect_punct('>')?;
Ok(name)
}
fn parse_tag_name(&mut self) -> ParseResult<TagName> {
if let Some(group) = self.peek_brace_group() {
let path = RustPath {
tokens: group.stream(),
};
self.position += 1;
return Ok(TagName::Component(path));
}
self.parse_xml_name().map(TagName::Element)
}
fn parse_xml_name(&mut self) -> ParseResult<XmlName> {
let first = self.expect_ident("expected XML name")?;
if self.consume_punct(':') {
let local = self.expect_ident("expected local name after namespace prefix")?;
Ok(XmlName {
prefix: Some(first),
local,
})
} else {
Ok(XmlName {
prefix: None,
local: first,
})
}
}
fn parse_attributes(&mut self) -> ParseResult<Vec<AttributeTemplate>> {
let mut attributes = Vec::new();
while !self.is_eof() && !self.peek_punct('>') && !self.peek_punct('/') {
let name = self.parse_xml_name()?;
self.expect_punct_with_message('=', "expected `=` after attribute name")?;
let value = self.parse_attribute_value()?;
attributes.push(AttributeTemplate { name, value });
}
Ok(attributes)
}
fn parse_attribute_value(&mut self) -> ParseResult<AttributeValue> {
match self.next() {
Some(TokenTree::Literal(literal)) => {
Ok(AttributeValue::Literal(literal_to_string(&literal)?))
}
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => {
Ok(AttributeValue::Expr(RustExpr {
tokens: group.stream(),
}))
}
_ => Err(ParseError::new(
"attribute value must be a string literal or `{ expr }`",
)),
}
}
fn parse_comment(&mut self) -> ParseResult<String> {
self.expect_punct('<')?;
self.expect_punct('!')?;
self.expect_punct('-')?;
self.expect_punct('-')?;
let mut tokens = Vec::new();
while !self.is_eof() {
if self.peek_punct('-') && self.peek_punct_at(1, '-') && self.peek_punct_at(2, '>') {
self.position += 3;
return Ok(tokens_to_text(tokens));
}
tokens.push(self.next().expect("checked not eof"));
}
Err(ParseError::new("unterminated XML comment"))
}
fn parse_text(&mut self) -> ParseResult<String> {
let mut tokens = Vec::new();
while !self.is_eof() && !self.peek_punct('<') && self.peek_brace_group().is_none() {
tokens.push(self.next().expect("checked not eof"));
}
if tokens.is_empty() {
return Err(ParseError::new("expected XML node"));
}
Ok(tokens_to_text(tokens))
}
fn expect_eof(&self) -> ParseResult<()> {
if self.is_eof() {
Ok(())
} else {
Err(ParseError::new("unexpected tokens after XML template"))
}
}
fn starts_comment(&self) -> bool {
self.peek_punct('<')
&& self.peek_punct_at(1, '!')
&& self.peek_punct_at(2, '-')
&& self.peek_punct_at(3, '-')
}
fn is_eof(&self) -> bool {
self.position >= self.tokens.len()
}
fn peek_punct(&self, expected: char) -> bool {
self.peek_punct_at(0, expected)
}
fn peek_punct_at(&self, offset: usize, expected: char) -> bool {
matches!(
self.tokens.get(self.position + offset),
Some(TokenTree::Punct(punct)) if punct.as_char() == expected
)
}
fn peek_brace_group(&self) -> Option<&Group> {
match self.tokens.get(self.position) {
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => Some(group),
_ => None,
}
}
fn consume_punct(&mut self, expected: char) -> bool {
if self.peek_punct(expected) {
self.position += 1;
true
} else {
false
}
}
fn expect_punct(&mut self, expected: char) -> ParseResult<()> {
self.expect_punct_with_message(expected, format!("expected `{expected}`"))
}
fn expect_punct_with_message(
&mut self,
expected: char,
message: impl Into<String>,
) -> ParseResult<()> {
if self.consume_punct(expected) {
Ok(())
} else {
Err(ParseError::new(message))
}
}
fn expect_ident(&mut self, message: impl Into<String>) -> ParseResult<String> {
match self.next() {
Some(TokenTree::Ident(ident)) => Ok(ident.to_string()),
_ => Err(ParseError::new(message)),
}
}
fn next(&mut self) -> Option<TokenTree> {
let token = self.tokens.get(self.position).cloned();
if token.is_some() {
self.position += 1;
}
token
}
}
#[derive(Debug, Clone)]
enum TagName {
Element(XmlName),
Component(RustPath),
}
impl TagName {
fn lexical(&self) -> String {
match self {
Self::Element(name) => name.lexical(),
Self::Component(path) => format!("{{{}}}", path.source()),
}
}
}
impl PartialEq for TagName {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Element(left), Self::Element(right)) => left == right,
(Self::Component(left), Self::Component(right)) => left.source() == right.source(),
_ => false,
}
}
}
impl Eq for TagName {}
fn tokens_to_text(tokens: Vec<TokenTree>) -> String {
tokens
.into_iter()
.map(|token| token.to_string())
.collect::<Vec<_>>()
.join(" ")
}
fn literal_to_string(literal: &Literal) -> ParseResult<String> {
let source = literal.to_string();
if let Some(value) = source
.strip_prefix('"')
.and_then(|rest| rest.strip_suffix('"'))
{
Ok(value.to_owned())
} else {
Err(ParseError::new("attribute literal must be a string"))
}
}
#[cfg(test)]
mod tests {
use proc_macro2::TokenStream;
use quote::quote;
use super::*;
use crate::ast::{AttributeValue, TemplateNode};
fn parse(tokens: TokenStream) -> ParseResult<Template> {
parse_template(tokens)
}
#[test]
fn macro_parser_parses_self_closing_element() {
let template = parse(quote! { <Root/> }).expect("valid template");
assert_eq!(template.nodes.len(), 1);
let TemplateNode::Element(root) = &template.nodes[0] else {
panic!("expected element");
};
assert_eq!(root.name.local, "Root");
assert!(root.self_closing);
}
#[test]
fn macro_parser_parses_nested_element_with_text() {
let template = parse(quote! { <Root><Child>text</Child></Root> }).expect("valid template");
let TemplateNode::Element(root) = &template.nodes[0] else {
panic!("expected root element");
};
let TemplateNode::Element(child) = &root.children[0] else {
panic!("expected child element");
};
let TemplateNode::Text(text) = &child.children[0] else {
panic!("expected text node");
};
assert_eq!(text, "text");
}
#[test]
fn macro_parser_parses_literal_and_expression_attributes() {
let template = parse(quote! { <Item code="A001" quantity={ item.quantity }/> })
.expect("valid template");
let TemplateNode::Element(item) = &template.nodes[0] else {
panic!("expected element");
};
assert_eq!(item.attributes.len(), 2);
assert_eq!(item.attributes[0].name.local, "code");
assert!(matches!(
&item.attributes[0].value,
AttributeValue::Literal(value) if value == "A001"
));
assert_eq!(item.attributes[1].name.local, "quantity");
assert!(matches!(
&item.attributes[1].value,
AttributeValue::Expr(expr) if expr.source() == "item . quantity"
));
}
#[test]
fn macro_parser_parses_expression_children() {
let template = parse(quote! { <ID>{ invoice_id }</ID> }).expect("valid template");
let TemplateNode::Element(id) = &template.nodes[0] else {
panic!("expected element");
};
let TemplateNode::Expr(expr) = &id.children[0] else {
panic!("expected expression child");
};
assert_eq!(expr.source(), "invoice_id");
}
#[test]
fn macro_parser_parses_prefixed_names_and_namespaces() {
let template = parse(quote! {
<doc:Root xmlns="urn:default" xmlns:doc="urn:doc" doc:id={ id }/>
})
.expect("valid template");
let TemplateNode::Element(root) = &template.nodes[0] else {
panic!("expected element");
};
assert_eq!(root.name.prefix.as_deref(), Some("doc"));
assert_eq!(root.name.local, "Root");
assert_eq!(root.attributes[1].name.prefix.as_deref(), Some("xmlns"));
assert_eq!(root.attributes[1].name.local, "doc");
assert_eq!(root.attributes[2].name.prefix.as_deref(), Some("doc"));
assert_eq!(root.attributes[2].name.local, "id");
}
#[test]
fn macro_parser_parses_comments() {
let template = parse(quote! { <Root><!-- hello --></Root> }).expect("valid template");
let TemplateNode::Element(root) = &template.nodes[0] else {
panic!("expected element");
};
let TemplateNode::Comment(comment) = &root.children[0] else {
panic!("expected comment");
};
assert_eq!(comment, "hello");
}
#[test]
fn macro_ast_represents_components_with_props_and_children() {
let template =
parse(quote! { <{InvoiceHeader} title="Demo"><ID>{ id }</ID></{InvoiceHeader}> })
.expect("valid template");
let TemplateNode::Component(component) = &template.nodes[0] else {
panic!("expected component");
};
assert_eq!(component.path.source(), "InvoiceHeader");
assert_eq!(component.props.len(), 1);
assert_eq!(component.children.len(), 1);
}
#[test]
fn macro_parser_rejects_mismatched_tags() {
let error = parse(quote! { <Root><Child></Root> }).expect_err("mismatched tags must fail");
assert!(error.message().contains("mismatched closing tag"));
assert!(error.message().contains("expected </Child>"));
}
#[test]
fn macro_parser_rejects_invalid_attribute_without_equals() {
let error = parse(quote! { <Root id/> }).expect_err("invalid attribute must fail");
assert!(error
.message()
.contains("expected `=` after attribute name"));
}
#[test]
fn macro_parser_rejects_unclosed_tags() {
let error = parse(quote! { <Root><Child/> }).expect_err("unclosed tag must fail");
assert!(error.message().contains("unclosed tag <Root>"));
}
#[test]
fn macro_parser_rejects_non_string_attribute_literals() {
let error = parse(quote! { <Root id=123/> }).expect_err("numeric attr literal must fail");
assert!(error
.message()
.contains("attribute literal must be a string"));
}
}