use crate::{asset::AssetParser, resolve_path}; use macro_string::MacroString; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseStream}, token::Comma, Ident, Token, Visibility, }; pub(crate) struct CssModuleParser { /// Whether the ident is const or static. styles_vis: Visibility, styles_ident: Ident, asset_parser: AssetParser, } impl Parse for CssModuleParser { fn parse(input: ParseStream) -> syn::Result { // NEW: macro!(pub? STYLES_IDENT = "/path.css"); // pub(x)? let styles_vis = input.parse::()?; // Styles Ident let styles_ident = input.parse::()?; let _equals = input.parse::()?; // Asset path "/path.css" let (MacroString(src), path_expr) = input.call(crate::parse_with_tokens)?; let asset = resolve_path(&src); let _comma = input.parse::(); // Optional options let mut options = input.parse::()?; if options.is_empty() { options = quote! { manganis::CssModuleAssetOptions::new() } } let asset_parser = AssetParser { path_expr, asset, options, }; Ok(Self { styles_vis, styles_ident, asset_parser, }) } } impl ToTokens for CssModuleParser { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { // Use the regular asset parser to generate the linker bridge. let mut linker_tokens = quote! { /// Auto-generated Manganis asset for css modules. #[allow(missing_docs)] const ASSET: manganis::Asset = }; self.asset_parser.to_tokens(&mut linker_tokens); let path = match self.asset_parser.asset.as_ref() { Ok(path) => path, Err(err) => { let err = err.to_string(); tokens.append_all(quote! { compile_error!(#err) }); return; } }; // Get the file hash let hash = match crate::hash_file_contents(path) { Ok(hash) => hash, Err(err) => { let err = err.to_string(); tokens.append_all(quote! { compile_error!(#err) }); return; } }; // Process css idents let css = std::fs::read_to_string(path).unwrap(); let (classes, ids) = manganis_core::collect_css_idents(&css); let mut values = Vec::new(); // Create unique module name based on styles ident. let styles_ident = &self.styles_ident; let mod_name = format_ident!("__{}_module", styles_ident); // Generate id struct field tokens. for id in ids.iter() { let as_snake = to_snake_case(id); let ident = Ident::new(&as_snake, Span::call_site()); values.push(quote! { pub const #ident: #mod_name::__CssIdent = #mod_name::__CssIdent { inner: manganis::macro_helpers::const_serialize::ConstStr::new(#id).push_str(#mod_name::__ASSET_HASH.as_str()).as_str() }; }); } // Generate class struct field tokens. for class in classes.iter() { let as_snake = to_snake_case(class); let as_snake = match ids.contains(class) { false => as_snake, true => format!("{as_snake}_class"), }; let ident = Ident::new(&as_snake, Span::call_site()); values.push(quote! { pub const #ident: #mod_name::__CssIdent = #mod_name::__CssIdent { inner: manganis::macro_helpers::const_serialize::ConstStr::new(#class).push_str(#mod_name::__ASSET_HASH.as_str()).as_str() }; }); } let options = &self.asset_parser.options; let styles_vis = &self.styles_vis; // We use a PhantomData to prevent Rust from complaining about an unused lifetime if a css module without any idents is used. tokens.extend(quote! { #[doc(hidden)] #[allow(missing_docs, non_snake_case)] mod #mod_name { #[allow(unused_imports)] use super::manganis::{self, CssModuleAssetOptions}; #linker_tokens; // Get the hash to use when builidng hashed css idents. const __ASSET_OPTIONS: manganis::AssetOptions = #options.into_asset_options(); pub(super) const __ASSET_HASH: manganis::macro_helpers::const_serialize::ConstStr = manganis::macro_helpers::hash_asset(&__ASSET_OPTIONS, #hash); // Css ident class for deref stylesheet inclusion. pub(super) struct __CssIdent { pub inner: &'static str } use std::ops::Deref; use std::sync::OnceLock; use dioxus::document::{document, LinkProps}; impl Deref for __CssIdent { type Target = str; fn deref(&self) -> &Self::Target { static CELL: OnceLock<()> = OnceLock::new(); CELL.get_or_init(move || { let doc = document(); doc.create_link( LinkProps::builder() .rel(Some("stylesheet".to_string())) .href(Some(ASSET.to_string())) .build(), ); }); self.inner } } impl std::fmt::Display for __CssIdent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { self.deref().fmt(f) } } } /// Auto-generated idents struct for CSS modules. #[allow(missing_docs, non_snake_case)] #styles_vis struct #styles_ident {} impl #styles_ident { #( #values )* } }) } } /// Convert camel and kebab case to snake case. /// /// This can fail sometimes, for example `myCss-Class`` is `my_css__class` fn to_snake_case(input: &str) -> String { let mut new = String::new(); for (i, c) in input.chars().enumerate() { if c.is_uppercase() && i != 0 { new.push('_'); } new.push(c.to_ascii_lowercase()); } new.replace('-', "_") }