use proc_macro2::{Span, TokenStream, TokenTree};
use quote::{quote, ToTokens};
use syn::parse::{ParseStream, Parser};
use syn::Token;
fn span_eq_hack(a: &Span, b: &Span) -> bool {
format!("{a:?}") == format!("{b:?}")
}
fn error_replace_span(err: syn::Error, from: Span, to: impl ToTokens) -> syn::Error {
let err_it = err.into_iter().map(|err| {
if span_eq_hack(&err.span(), &from) {
syn::Error::new_spanned(&to, err.to_string())
} else {
err
}
});
crate::join_errors(err_it).unwrap_err()
}
pub struct TagTokens {
pub lt: Token![<],
pub div: Option<Token![/]>,
pub gt: Token![>],
}
impl TagTokens {
pub fn parse_start_content<T>(
input: ParseStream,
parse: impl FnOnce(ParseStream, Self) -> syn::Result<T>,
) -> syn::Result<T> {
Self::parse_content(Self::parse_start(input)?, parse)
}
pub fn parse_end_content<T>(
input: ParseStream,
parse: impl FnOnce(ParseStream, Self) -> syn::Result<T>,
) -> syn::Result<T> {
Self::parse_content(Self::parse_end(input)?, parse)
}
fn parse_content<T>(
(tag, content): (Self, TokenStream),
parse: impl FnOnce(ParseStream, Self) -> syn::Result<T>,
) -> syn::Result<T> {
let scope_spanned = tag.to_spanned();
let content_parser = |input: ParseStream| {
parse(input, tag).map_err(|err| {
error_replace_span(err, Span::call_site(), &scope_spanned)
})
};
content_parser.parse2(content)
}
fn parse_start(input: ParseStream) -> syn::Result<(Self, TokenStream)> {
let lt = input.parse()?;
let (content, div, gt) = Self::parse_until_end(input)?;
Ok((Self { lt, div, gt }, content))
}
fn parse_end(input: ParseStream) -> syn::Result<(Self, TokenStream)> {
let lt = input.parse()?;
let div = Some(input.parse()?);
let (content, end_div, gt) = Self::parse_until_end(input)?;
if end_div.is_some() {
return Err(syn::Error::new_spanned(
end_div,
"unexpected `/` in this end tag",
));
}
Ok((Self { lt, div, gt }, content))
}
fn parse_until_end(
input: ParseStream,
) -> syn::Result<(TokenStream, Option<Token![/]>, Token![>])> {
let mut inner_trees = Vec::new();
let mut angle_count: usize = 1;
let mut div: Option<Token![/]> = None;
let gt: Token![>];
loop {
let next = input.parse()?;
if let TokenTree::Punct(punct) = &next {
match punct.as_char() {
'/' if angle_count == 1 && input.peek(Token![>]) => {
div = Some(syn::token::Slash {
spans: [punct.span()],
});
gt = input.parse()?;
break;
}
'>' => {
angle_count = angle_count.checked_sub(1).ok_or_else(|| {
syn::Error::new_spanned(
punct,
"this tag close has no corresponding tag open",
)
})?;
if angle_count == 0 {
gt = syn::token::Gt {
spans: [punct.span()],
};
break;
}
}
'<' => angle_count += 1,
_ => {}
};
}
inner_trees.push(next);
}
Ok((inner_trees.into_iter().collect(), div, gt))
}
pub fn to_spanned(&self) -> impl ToTokens {
let Self { lt, gt, .. } = self;
quote! {#lt #gt}
}
}