extern crate proc_macro;
use proc_macro::TokenStream;
use crate::page_wrapper::PageWrapper;
use quote::{format_ident, quote, ToTokens};
use syn::parse_macro_input;
use tokens::Component;
use uuid::Uuid;
mod code_generation;
mod i18n;
mod page_wrapper;
mod parsing;
mod tokens;
#[doc = include_str!("view.md")]
#[allow(clippy::all)]
#[proc_macro]
pub fn view(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as Component);
let expanded = input.to_token_stream();
expanded.into()
}
#[allow(clippy::all)]
#[proc_macro]
pub fn page(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as PageWrapper);
let expanded = input.to_token_stream();
expanded.into()
}
#[allow(clippy::all)]
#[proc_macro]
pub fn head(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as Component);
let x = Uuid::new_v4().to_string();
let input = quote!(
page.add_elements_to_head(#x, #input);
);
let expanded = input.to_token_stream();
expanded.into()
}
#[allow(clippy::all)]
#[proc_macro]
pub fn scoped_css(input: TokenStream) -> TokenStream {
let file_name = input.to_string().replace("\"", "");
let x = format!("tidos-{}", Uuid::new_v4().to_string());
let input = quote!(
{
page.add_elements_to_head(#x, String::from(concat!("<style>.", #x, " {", include_str!(#file_name), "}</style>")));
#x
}
);
let expanded = input.to_token_stream();
expanded.into()
}
#[allow(clippy::all)]
#[proc_macro_attribute]
pub fn native_element(_args: TokenStream, input: TokenStream) -> TokenStream {
let input_struct = parse_macro_input!(input as syn::ItemStruct);
let struct_name = &input_struct.ident;
let struct_name_str = struct_name.to_string();
let kebab_name = pascal_to_kebab(&struct_name_str);
if !kebab_name.contains('-') {
return syn::Error::new_spanned(
&input_struct.ident,
format!(
"#[native_element] generated the tag name '{}', which is not a valid custom \
element name — custom elements must contain at least one '-'. \
Use a multi-word struct name (e.g. 'App{}' → 'app-{}').",
kebab_name, struct_name_str, kebab_name
),
)
.to_compile_error()
.into();
}
let tag_name: Vec<syn::Ident> = kebab_name
.split("-")
.map(|x| format_ident!("{}", x))
.collect();
let tag_name = quote! { #( #tag_name )-* };
let script_src = format!("/dist/{}.js", struct_name_str);
let fields: Vec<&syn::Field> = match &input_struct.fields {
syn::Fields::Named(f) => f.named.iter().collect(),
syn::Fields::Unit => vec![],
_ => {
return syn::Error::new_spanned(
&input_struct.ident,
"#[native_element] only supports structs with named fields or unit structs",
)
.to_compile_error()
.into()
}
};
let attr_stmts: Vec<proc_macro2::TokenStream> = fields
.iter()
.map(|field| {
let field_ident = field.ident.as_ref().unwrap();
let attr_parts: Vec<syn::Ident> = field_ident
.to_string()
.split("_")
.map(|x| format_ident!("{}", x))
.collect();
let attr_name = quote! { #( #attr_parts )-* };
if is_bool_type(&field.ty) {
quote! {
:#attr_name={self.#field_ident}
}
} else {
quote! {
#attr_name={self.#field_ident}
}
}
})
.collect();
let expanded = quote! {
#input_struct
impl tidos::Component for #struct_name {
fn to_render(&self, page: &mut tidos::Page) -> String {
tidos::head!(<script r#type="module" src=#script_src></script>);
tidos::view!(<#tag_name #( #attr_stmts )*></#tag_name>)
}
}
};
expanded.into()
}
fn pascal_to_kebab(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i > 0 {
result.push('-');
}
result.push(c.to_ascii_lowercase());
}
result
}
fn is_bool_type(ty: &syn::Type) -> bool {
if let syn::Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "bool";
}
}
false
}