#![doc(hidden)]
use proc_macro::TokenStream;
use rshtml_core::{Compiler, v_macro};
use std::path::Path;
use syn::{Data, DeriveInput, Fields, LitBool, LitStr, Token, parse_macro_input};
#[proc_macro_derive(View, attributes(view))]
pub fn view_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = input.ident;
let struct_generics = input.generics;
let struct_fields = get_struct_fields(&input.data);
let (template_path, extract) = match parse_template_path_from_attrs(&input.attrs) {
Ok(rshtml_config) => {
if let Some(path) = rshtml_config.path {
(path, rshtml_config.extract)
} else {
let struct_name_str = struct_name.to_string();
let mut template_file = if let Some(stripped) = struct_name_str.strip_suffix("Page")
{
format!("{stripped}.rs.html")
} else {
format!("{struct_name_str}.rs.html")
};
template_file = to_snake_case(&template_file);
(format!("views/{template_file}"), rshtml_config.extract)
}
}
Err(err) => {
return err.to_compile_error().into();
}
};
let mut compiler = Compiler::new(struct_name, struct_generics, struct_fields, extract);
let path = Path::new(&template_path);
TokenStream::from(compiler.compile(path))
}
struct RsHtmlConfig {
pub path: Option<String>,
pub extract: bool,
}
fn parse_template_path_from_attrs(attrs: &[syn::Attribute]) -> syn::Result<RsHtmlConfig> {
let mut config = RsHtmlConfig {
path: None,
extract: false,
};
for attr in attrs {
if attr.path().is_ident("view") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("path") {
let value = meta.value()?;
let s: LitStr = value.parse()?;
config.path = Some(s.value());
return Ok(());
}
if meta.path.is_ident("extract") {
if meta.input.peek(Token![=]) {
let value = meta.value()?;
let lit: LitBool = value.parse()?;
config.extract = lit.value();
} else {
config.extract = true;
}
return Ok(());
}
Err(meta.error("unsupported rshtml property"))
})?;
}
}
Ok(config)
}
fn to_snake_case(s: &str) -> String {
if s.is_empty() {
return String::new();
}
let mut result = String::new();
let chars = s.chars().peekable();
for c in chars {
if c.is_uppercase() {
if !result.is_empty() {
result.push('_');
}
result.push(c.to_ascii_lowercase());
} else {
result.push(c);
}
}
result
}
fn get_struct_fields(data: &Data) -> Vec<String> {
match data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(fields_named) => fields_named
.named
.iter()
.filter_map(|f| f.ident.as_ref().map(|id| id.to_string()))
.collect(),
Fields::Unnamed(fields_unnamed) => fields_unnamed
.unnamed
.iter()
.enumerate()
.map(|(index, _field)| index.to_string())
.collect(),
_ => Vec::new(),
},
_ => Vec::new(),
}
}
#[proc_macro]
pub fn v(input: TokenStream) -> TokenStream {
TokenStream::from(v_macro::compile(input.into()))
}