1use lightningcss::{printer::PrinterOptions, properties::Property};
2use proc_macro::TokenStream;
3use proc_macro2::TokenStream as TokenStream2;
4use quote::quote;
5use syn::{parse_macro_input, Field, Fields, ItemStruct, LitStr};
6use titan_utils::validatecss::{
7 validate_css, validate_globalcss, CSSValidationError,
8};
9
10fn from_stylerules_to_tokenstream(
11 prop: Vec<(String, Property<'_>)>,
12) -> TokenStream2 {
13 let styles_tokens: Vec<TokenStream2> = prop
14 .iter()
15 .map(|(hash, prop)| {
16 let prop_id = prop.property_id();
17 let key = prop_id.name();
18
19 let value = prop.value_to_css_string(PrinterOptions::default()).unwrap();
20
21 quote::quote! {
22 titan::html::StyleRule {
23 rule: #hash,
24 styles: &[(#key, #value)],
25 }
26 }
27 })
28 .collect();
29
30 quote::quote! {
31 #(#styles_tokens),*
32 }
33}
34
35#[proc_macro]
36pub fn global_css(input: TokenStream) -> TokenStream {
37 let input = parse_macro_input!(input as LitStr);
40 let result = input.value();
41
42 let err = validate_globalcss(&result);
43
44 quote! { titan::html::tags::Style::Text(#err.to_string()) }.into()
45}
46
47#[proc_macro]
48pub fn css(input: TokenStream) -> TokenStream {
49 let input = parse_macro_input!(input as LitStr);
52 let result = input.value();
53
54 if let Err(err) = validate_css(&result) {
55 match err {
56 CSSValidationError::FieldError(field) => {
57 let span = input.span();
58
59 let error_msg = format!("Invalid css property name: {}", field);
60
61 let err = syn::Error::new(span, error_msg);
62 return err.to_compile_error().into(); }
64 CSSValidationError::EntireFile(location) => {
65 let span = input.span();
66
67 let error_msg = format!(
68 "Error parsing css at line = {}, col = {}",
69 location.line, location.column
70 );
71
72 let err = syn::Error::new(span, error_msg);
73 return err.to_compile_error().into(); }
75 }
76 };
77
78 let rules = titan_html_core::parse_css_block(&result);
79
80 let rules = from_stylerules_to_tokenstream(rules);
81
82 quote! { &[#rules] }.into()
83}
84
85#[proc_macro]
86pub fn html_tag(item: TokenStream) -> TokenStream {
87 let mut item_struct = syn::parse::<ItemStruct>(item).unwrap();
88 let struct_name = &item_struct.ident;
89
90 let new_field: Vec<Field> = Vec::from_iter([
91 syn::parse_quote! { pub classes: std::collections::HashSet<crate::tags::TagClass> },
92 syn::parse_quote! { pub ids: Vec<String> },
93 syn::parse_quote! { pub attributes: HashMap<String, String> },
94 ]);
95
96 let Fields::Named(fields_named) = &mut item_struct.fields else {
97 return syn::Error::new_spanned(
98 item_struct,
99 "Only named fields are supported",
100 )
101 .to_compile_error()
102 .into();
103 };
104
105 for field in new_field {
107 fields_named.named.push(field);
108 }
109
110 let expanded = quote! {
111 #[derive(Clone)]
112 #item_struct
113
114 impl #struct_name {
115 pub fn class(mut self, class: impl Into<String>) -> Self {
116 let classes: Vec<String> = class.into().split(' ').map(|x| x.to_string()).collect();
117
118 for class in classes {
119 let key = crate::tags::TagClass::text(class.clone());
120 if self.classes.contains(&key) {
121 eprintln!("warning: class '{class}' already is defined in element: {}", stringify!(#struct_name));
122 }
123 self.classes.insert(key);
124 }
125 self
126 }
127
128 pub fn id(mut self, id: impl Into<String>) -> Self {
129 self.ids.push(id.into());
130 self
131 }
132
133 pub fn add_attribute(mut self, key: String, value: String) -> Self {
134 self.attributes.insert(key, value);
135 self
136 }
137
138 pub fn styles(mut self, style_rules: &[titan_html_core::StyleRule]) -> Self {
139 for style in style_rules {
140 self.classes.insert(crate::tags::TagClass::StyleRule(style.clone()));
141 };
142 self
143 }
144 }
145 };
146
147 TokenStream::from(expanded)
148}