use crate::config::{HtmlConfig, ParsedHtmlConfigField};
use crate::maybe_dollar::PathAndMaybeDollar;
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::discouraged::Speculative;
use syn::parse::{Parse, ParseStream};
use syn::{LitStr, Token};
pub(super) struct CreateHtmlMacro {
name: syn::Ident,
doc: Vec<LitStr>,
html_macro_path: Option<PathAndMaybeDollar>,
html_config: HtmlConfig,
}
impl Parse for CreateHtmlMacro {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut doc = Vec::new();
while let Ok(_) = input.parse::<Token![#]>() {
let content;
syn::bracketed!(content in input);
match content.parse::<syn::Ident>()?.to_string().as_str() {
"doc" => {
content.parse::<Token![=]>()?;
let doc_line = content.parse::<LitStr>()?;
doc.push(doc_line);
}
attrib => {
todo!("return error indicating that {attrib} is an unsupported attribute")
}
}
}
let name = input.parse::<syn::Ident>()?;
input.parse::<Token![!]>()?;
let mut html_macro_path = None;
let mut html_config_fields = Vec::new();
loop {
if input.is_empty() {
break;
}
let parse_field = input.fork();
match input.parse::<syn::Ident>()?.to_string().as_str() {
"calls" => {
input.parse::<Token![=]>()?;
html_macro_path = Some(input.parse::<PathAndMaybeDollar>()?);
}
_ => {
let field = ParsedHtmlConfigField::parse(&parse_field)?;
html_config_fields.push(field);
input.advance_to(&parse_field);
}
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
let html_config = HtmlConfig::from_fields(html_config_fields);
Ok(CreateHtmlMacro {
name,
doc,
html_macro_path,
html_config,
})
}
}
impl CreateHtmlMacro {
pub fn into_tokens(self) -> TokenStream {
let CreateHtmlMacro {
name,
doc,
html_macro_path,
html_config: config,
} = self;
let HtmlConfig { real_dom_ty } = config;
let doc = doc.into_iter().map(|doc_line| {
quote! {
#[doc = #doc_line]
}
});
let html_macro_path = html_macro_path.unwrap_or_else(|| {
syn::parse2(quote! {
html_with_config
})
.unwrap()
});
quote! {
#(#doc)*
#[macro_export]
macro_rules! #name {
($($tokens:tt)*) => {
#html_macro_path! {
real_dom = #real_dom_ty,
;
$($tokens)*
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
#[test]
fn define_html_macro_with_real_dom() {
let tokens = quote! {
my_html!
real_dom = SomeType
};
let expected = quote! {
#[macro_export]
macro_rules! my_html {
($($tokens:tt)*) => {
html_with_config! {
real_dom = SomeType,
;
$($tokens)*
}
}
}
};
assert_expected_create_html_tokens(tokens, expected);
}
#[test]
fn allows_trailing_comma() {
let tokens = quote! {
my_html!
real_dom = SomeType,
};
let expected = quote! {
#[macro_export]
macro_rules! my_html {
($($tokens:tt)*) => {
html_with_config! {
real_dom = SomeType,
;
$($tokens)*
}
}
}
};
assert_expected_create_html_tokens(tokens, expected);
}
#[test]
fn configure_html_macro_path() {
let tokens = quote! {
my_html!
real_dom = SomeType,
calls = path::to::html_with_config
};
let expected = quote! {
#[macro_export]
macro_rules! my_html {
($($tokens:tt)*) => {
path::to::html_with_config! {
real_dom = SomeType,
;
$($tokens)*
}
}
}
};
assert_expected_create_html_tokens(tokens, expected);
}
#[test]
fn sets_documentation() {
let tokens = quote! {
my_html!
real_dom = SomeType,
};
let expected = quote! {
#[macro_export]
macro_rules! my_html {
($($tokens:tt)*) => {
html_with_config! {
real_dom = SomeType,
;
$($tokens)*
}
}
}
};
assert_expected_create_html_tokens(tokens, expected);
}
#[test]
fn supports_dollar_crate() {
let tokens = quote! {
my_html!
real_dom = $crate::path::to::SomeType,
calls = $crate::path::to::html_with_config
};
let expected = quote! {
#[macro_export]
macro_rules! my_html {
($($tokens:tt)*) => {
$crate::path::to::html_with_config! {
real_dom = $crate::path::to::SomeType,
;
$($tokens)*
}
}
}
};
assert_expected_create_html_tokens(tokens, expected);
}
#[track_caller]
fn assert_expected_create_html_tokens(start: TokenStream, expected: TokenStream) {
let html: CreateHtmlMacro = syn::parse2(start).unwrap();
assert_eq!(html.into_tokens().to_string(), expected.to_string());
}
}