titan_html_derive/
lib.rs

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  // Parse the input into a string literal
38
39  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  // Parse the input into a string literal
50
51  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(); // Return the error as a TokenStream
63      }
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(); // Return the error as a TokenStream
74      }
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  // Append the new field to the struct
106  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}