use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{Error, Result};
mod ast;
mod tests;
#[proc_macro]
pub fn defy(ts: proc_macro::TokenStream) -> proc_macro::TokenStream {
run(ts.into()).unwrap_or_else(Error::into_compile_error).into()
}
struct Config {
debug_print: bool,
macro_path: syn::Path,
}
fn run(ts: TokenStream) -> Result<TokenStream> {
let input: ast::Input = syn::parse2(ts)?;
let mut config =
Config { debug_print: false, macro_path: syn::parse2(quote!(::yew::html)).unwrap() };
for ast_config in input.configs {
match ast_config {
ast::Config::DebugPrint { at: _, kw: _ } => config.debug_print = true,
ast::Config::MacroPath { at: _, kw: _, path } => config.macro_path = path,
}
}
let output = emit(&config.macro_path, Span::call_site(), input.nodes)?;
if config.debug_print {
println!("{output}")
}
Ok(output)
}
fn emit(macro_path: &syn::Path, span: Span, nodes: ast::Nodes) -> Result<TokenStream> {
let mut stmts = nodes.stmts.into_iter().peekable();
let mut locals = Vec::new();
while let Some(ast::Stmt::Let(..)) = stmts.peek() {
let ast::Let { let_, pat, eq, expr, semi } = match stmts.next() {
Some(ast::Stmt::Let(stmt)) => stmt,
_ => unreachable!(),
};
locals.push(quote_spanned! { let_.span() =>
#let_ #pat #eq #expr #semi
});
}
let node_html: Vec<_> =
stmts.map(|stmt| stmt_to_html(macro_path, stmt)).collect::<Result<_>>()?;
Ok(quote_spanned! { span =>
#(#locals)*
#macro_path! {
<>
#(#node_html)*
</>
}
})
}
fn stmt_to_html(macro_path: &syn::Path, stmt: ast::Stmt) -> Result<TokenStream> {
Ok(match stmt {
ast::Stmt::If(ast::If {
if_,
expr,
braces: if_braces,
body: if_body,
else_: Some(ast::Else { else_, braces: else_braces, body: else_body }),
}) => {
let if_body = emit(macro_path, if_braces.span.join(), if_body)?;
let if_part = quote_spanned! { if_braces.span =>
#if_ #expr { #if_body }
};
let else_body = emit(macro_path, else_braces.span.join(), else_body)?;
let else_part = quote_spanned! { else_braces.span =>
#else_ { #else_body }
};
quote_spanned! { if_.span() =>
{ #if_part #else_part }
}
}
ast::Stmt::If(ast::If { if_, expr, braces, body, else_: None }) => {
let body = emit(macro_path, braces.span.join(), body)?;
quote_spanned! { if_.span() =>
{ #if_ #expr { #body } else { #macro_path! {} } }
}
}
ast::Stmt::Match(ast::Match { match_, expr, braces, arms }) => {
let arms: TokenStream = arms
.into_iter()
.map(|ast::Arm { pat, guard, fat_arrow, braces, body }| {
let guard = guard.map(|(if_, expr)| quote!(#if_ #expr));
let body = emit(macro_path, braces.span.join(), body)?;
Ok(quote_spanned! { braces.span =>
#pat #guard #fat_arrow { #body }
})
})
.collect::<Result<_>>()?;
quote_spanned! { braces.span =>
{ #match_ #expr {
#arms
} }
}
}
ast::Stmt::For(ast::For { for_, pat, iter, in_, braces, body }) => {
let body = emit(macro_path, braces.span.join(), body)?;
quote_spanned! { in_.span() =>
{ #for_ ::std::iter::IntoIterator::into_iter(#iter).map(|#pat| { #body }) }
}
}
ast::Stmt::Let(ast::Let { let_, .. }) => {
return Err(Error::new_spanned(
let_,
"let statements must precede all other statements in a block",
))
}
ast::Stmt::Text(ast::Text { add, expr, semi: _ }) => {
quote_spanned! { add.span =>
{ #expr }
}
}
ast::Stmt::Node(ast::Node { element, args, body }) => {
let args = args_to_html(args)?;
match body {
ast::NodeBody::Semi(semi) => quote_spanned! { semi.span =>
<#element #args />
},
ast::NodeBody::Braced { braces, children } => {
let children = emit(macro_path, braces.span.join(), children)?;
quote_spanned! { braces.span =>
<#element #args>
{ #children }
</#element>
}
}
}
}
})
}
fn args_to_html(args: ast::NodeArgs) -> Result<TokenStream> {
Ok(match args {
ast::NodeArgs::None => TokenStream::new(),
ast::NodeArgs::Named { paren: _, args } => args
.into_iter()
.map(|ast::NodeArg { ident, value }| match value {
None => quote_spanned! { ident.span() =>
{#ident}
},
Some((eq, value)) => quote_spanned! { eq.span =>
#ident = {#value}
},
})
.collect(),
ast::NodeArgs::Rest { eq, arg } => quote_spanned! { eq.span =>
..#arg
},
})
}