use proc_macro2::{Span, TokenStream, TokenTree};
use quote::{quote_spanned, ToTokens};
use syn::parse::{Parse, ParseStream, Result};
use syn::spanned::Spanned;
use syn::token::Brace;
use syn::{braced, Block, Expr, Ident, Token};
#[derive(Debug)]
pub enum Tag {
Open {
name: Ident,
attrs: Vec<Attr>,
open_bracket_span: Span,
closing_bracket_span: Span,
is_self_closing: bool,
},
Close {
name: Ident,
first_angle_bracket_span: Span,
},
Text {
text: String,
start_span: Option<Span>,
end_span: Option<Span>,
},
Braced { block: Box<Block>, brace_span: Span },
}
#[derive(Debug, Eq, PartialEq)]
pub enum TagKind {
Open,
Close,
Text,
Braced,
}
#[derive(Debug)]
pub struct Attr {
key: TokenStream,
key_span: Span,
value: Expr,
}
impl Attr {
pub fn key_string(&self) -> String {
self.key.to_string().replace(" ", "")
}
pub fn key_span(&self) -> Span {
self.key_span
}
pub fn value(&self) -> &Expr {
&self.value
}
}
impl Parse for Tag {
fn parse(input: ParseStream) -> Result<Self> {
let mut input = input;
if input.peek(Token![<]) {
let first_angle_bracket_span = input.parse::<Token![<]>()?;
let first_angle_bracket_span = first_angle_bracket_span.span();
let optional_close: Option<Token![/]> = input.parse()?;
let is_open_tag = optional_close.is_none();
if is_open_tag {
return parse_open_tag(&mut input, first_angle_bracket_span);
} else {
return parse_close_tag(&mut input, first_angle_bracket_span);
}
}
if input.peek(Brace) {
return parse_block(&mut input);
}
return parse_text_node(&mut input);
}
}
fn parse_open_tag(input: &mut ParseStream, open_bracket_span: Span) -> Result<Tag> {
let name: Ident = input.parse()?;
let attrs = parse_attributes(input)?;
let is_self_closing: Option<Token![/]> = input.parse()?;
let is_self_closing = is_self_closing.is_some();
let closing_bracket = input.parse::<Token![>]>()?;
let closing_bracket_span = closing_bracket.span();
Ok(Tag::Open {
name,
attrs,
open_bracket_span,
closing_bracket_span,
is_self_closing,
})
}
fn parse_attributes(input: &mut ParseStream) -> Result<Vec<Attr>> {
let mut attrs = Vec::new();
while input.peek(Ident)
|| input.peek(Token![as])
|| input.peek(Token![async])
|| input.peek(Token![for])
|| input.peek(Token![loop])
|| input.peek(Token![type])
{
let (key, key_span) = parse_attribute_key(input)?;
input.parse::<Token![=]>()?;
let mut value_tokens = TokenStream::new();
loop {
let tt: TokenTree = input.parse()?;
value_tokens.extend(Some(tt));
let next_token_is_attrib_key = input.peek(Ident)
|| input.peek(Token![as])
|| input.peek(Token![async])
|| input.peek(Token![for])
|| input.peek(Token![loop])
|| input.peek(Token![type]);
let next_next_token_is_equals_or_hyphen =
input.peek2(Token![=]) || input.peek2(Token![-]);
let peek_start_of_next_attr =
next_token_is_attrib_key && next_next_token_is_equals_or_hyphen;
let peek_end_of_tag = input.peek(Token![>]);
let peek_self_closing = input.peek(Token![/]);
if peek_end_of_tag || peek_start_of_next_attr || peek_self_closing {
break;
}
}
let value: Expr = syn::parse2(value_tokens)?;
attrs.push(Attr {
key,
key_span,
value,
});
}
Ok(attrs)
}
fn parse_attribute_key(input: &mut ParseStream) -> Result<(TokenStream, Span)> {
let first_key_segment = parse_attribute_key_segment(input)?;
let maybe_hyphen: Option<Token![-]> = input.parse()?;
let attribute_key;
if let Some(hyphen) = maybe_hyphen {
let next_segment = parse_attribute_key_segment(input)?;
let combined_span = first_key_segment
.span()
.join(hyphen.span())
.unwrap()
.join(next_segment.span())
.unwrap();
attribute_key = (
quote_spanned! {combined_span=> #first_key_segment - #next_segment },
combined_span,
);
} else {
attribute_key = (
first_key_segment.to_token_stream(),
first_key_segment.span(),
);
}
Ok(attribute_key)
}
fn parse_attribute_key_segment(input: &mut ParseStream) -> Result<Ident> {
let maybe_as_key: Option<Token![as]> = input.parse()?;
let maybe_async_key: Option<Token![async]> = input.parse()?;
let maybe_for_key: Option<Token![for]> = input.parse()?;
let maybe_loop_key: Option<Token![loop]> = input.parse()?;
let maybe_type_key: Option<Token![type]> = input.parse()?;
let key = if let Some(as_key) = maybe_as_key {
Ident::new("as", as_key.span())
} else if let Some(async_key) = maybe_async_key {
Ident::new("async", async_key.span())
} else if let Some(for_key) = maybe_for_key {
Ident::new("for", for_key.span())
} else if let Some(loop_key) = maybe_loop_key {
Ident::new("loop", loop_key.span())
} else if let Some(type_key) = maybe_type_key {
Ident::new("type", type_key.span())
} else {
input.parse()?
};
Ok(key)
}
fn parse_close_tag(input: &mut ParseStream, first_angle_bracket_span: Span) -> Result<Tag> {
let name: Ident = input.parse()?;
input.parse::<Token![>]>()?;
Ok(Tag::Close {
name,
first_angle_bracket_span,
})
}
fn parse_block(input: &mut ParseStream) -> Result<Tag> {
let content;
let brace_token = braced!(content in input);
let brace_span = brace_token.span.open();
let block_expr = content.call(Block::parse_within)?;
let block = Box::new(Block {
brace_token,
stmts: block_expr,
});
Ok(Tag::Braced { block, brace_span })
}
fn parse_text_node(input: &mut ParseStream) -> Result<Tag> {
let _text_tokens = TokenStream::new();
let mut text = "".to_string();
let mut idx = 0;
let mut start_span = None;
let mut most_recent_span: Option<Span> = None;
loop {
if input.is_empty() {
break;
}
let tt: TokenTree = input.parse()?;
if idx == 0 {
start_span = Some(tt.span());
most_recent_span = Some(tt.span());
}
if idx != 0 {
if let Some(most_recent_span) = most_recent_span {
let current_span_start = tt.span().start();
let most_recent_span_end = most_recent_span.end();
let spans_on_different_lines = current_span_start.line != most_recent_span_end.line;
let span_comes_before_previous_span = current_span_start.column
< most_recent_span_end.column
&& !spans_on_different_lines;
if spans_on_different_lines {
text += " ";
} else if !span_comes_before_previous_span
&& current_span_start.column - most_recent_span_end.column > 0
{
text += " ";
}
}
}
text += &tt.to_string();
most_recent_span = Some(tt.span());
let peek_closing_tag = input.peek(Token![<]);
let peek_start_block = input.peek(Brace);
if peek_closing_tag || peek_start_block {
break;
}
idx += 1;
}
Ok(Tag::Text {
text,
start_span,
end_span: most_recent_span,
})
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
use std::collections::HashMap;
use syn::Lit;
#[test]
fn open_tag_tests() {
let tests = [
(
quote! { <div> },
ExpectedTag {
name: "div",
attributes: vec![],
is_self_closing: false,
},
),
(
quote! { <br /> },
ExpectedTag {
name: "br",
attributes: vec![],
is_self_closing: true,
},
),
(
quote! { <div id="hello"> },
ExpectedTag {
name: "div",
attributes: vec![("id", "hello")],
is_self_closing: false,
},
),
(
quote! { <meta http-equiv="refresh" /> },
ExpectedTag {
name: "meta",
attributes: vec![("http-equiv", "refresh")],
is_self_closing: true,
},
),
(
quote! {
<path
d="M1,5 a2,2"
stroke-linejoin="miter"
/>
},
ExpectedTag {
name: "path",
attributes: vec![("d", "M1,5 a2,2"), ("stroke-linejoin", "miter")],
is_self_closing: true,
},
),
];
for (tokens, expected_tag) in tests {
let tokens_string = tokens.to_string();
let tag: Tag = syn::parse2(tokens).unwrap();
match tag {
Tag::Open {
name,
attrs,
is_self_closing,
..
} => {
assert_eq!(&name.to_string(), expected_tag.name, "{}", tokens_string);
assert_eq!(
is_self_closing, expected_tag.is_self_closing,
"{}",
tokens_string
);
let expected_attrs: HashMap<&'static str, &'static str> =
expected_tag.attributes.into_iter().collect();
assert_eq!(attrs.len(), expected_attrs.len(), "{}", tokens_string);
for attr in attrs {
let attr_key = attr.key_string();
let Expr::Lit(attr_val) = attr.value else {
panic!()
};
let Lit::Str(attr_val_str) = attr_val.lit else {
panic!()
};
let expected_val = expected_attrs
.get(attr_key.as_str())
.map(|val| val.to_string());
assert_eq!(Some(attr_val_str.value()), expected_val,);
}
}
not_open => panic!("Should have been an open tag. {:?}", not_open),
}
}
}
struct ExpectedTag {
name: &'static str,
attributes: Vec<(&'static str, &'static str)>,
is_self_closing: bool,
}
}