Scoped css (#5087)
* Create new css module parser * Use css module processing * Make hot reloading work for css_module macro * Change css_module macro shape and rename to styles * Add css_module example * Update styles macro documentation * Remove hidden from styles macro * Remove unneeded param * Remove unneeded visibility * Improve `styles!` macro documentation * Clippy * Remove unneeded method * Clippy * Clippy * Replace panic with anyhow * Update css_module macro to attribute macro * Update macro docs * chore: Cargo.lock * Clippy * respect semver by deprecating old func * update to latest winnow --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
34554766db
commit
9965220947
14 files changed
+978
-286
No files matched your search
@@ -1,31 +1,21 @@
|
||||
use crate::{asset::AssetParser, resolve_path};
|
||||
use macro_string::MacroString;
|
||||
use manganis_core::{create_module_hash, get_class_mappings};
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
spanned::Spanned,
|
||||
token::Comma,
|
||||
Ident, Token, Visibility,
|
||||
Ident, ItemStruct,
|
||||
};
|
||||
|
||||
pub(crate) struct CssModuleParser {
|
||||
/// Whether the ident is const or static.
|
||||
styles_vis: Visibility,
|
||||
styles_ident: Ident,
|
||||
pub(crate) struct CssModuleAttribute {
|
||||
asset_parser: AssetParser,
|
||||
}
|
||||
|
||||
impl Parse for CssModuleParser {
|
||||
impl Parse for CssModuleAttribute {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
// NEW: macro!(pub? STYLES_IDENT = "/path.css");
|
||||
// pub(x)?
|
||||
let styles_vis = input.parse::<Visibility>()?;
|
||||
|
||||
// Styles Ident
|
||||
let styles_ident = input.parse::<Ident>()?;
|
||||
let _equals = input.parse::<Token![=]>()?;
|
||||
|
||||
// Asset path "/path.css"
|
||||
let (MacroString(src), path_expr) = input.call(crate::parse_with_tokens)?;
|
||||
let asset = resolve_path(&src, path_expr.span());
|
||||
@@ -44,136 +34,127 @@ impl Parse for CssModuleParser {
|
||||
options,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
styles_vis,
|
||||
styles_ident,
|
||||
asset_parser,
|
||||
})
|
||||
Ok(Self { 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 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 )*
|
||||
}
|
||||
})
|
||||
pub(crate) fn expand_css_module_struct(
|
||||
tokens: &mut proc_macro2::TokenStream,
|
||||
attribute: &CssModuleAttribute,
|
||||
item_struct: &ItemStruct,
|
||||
) {
|
||||
if !item_struct.fields.is_empty() {
|
||||
let err = syn::Error::new(
|
||||
item_struct.fields.span(),
|
||||
"css_module can only be applied to unit structs",
|
||||
)
|
||||
.into_compile_error();
|
||||
tokens.append_all(err);
|
||||
return;
|
||||
}
|
||||
if !item_struct.generics.params.is_empty() {
|
||||
let err = syn::Error::new(
|
||||
item_struct.generics.span(),
|
||||
"css_module cannot be applied to generic structs",
|
||||
)
|
||||
.into_compile_error();
|
||||
tokens.append_all(err);
|
||||
return;
|
||||
}
|
||||
let struct_vis = &item_struct.vis;
|
||||
let struct_name = &item_struct.ident;
|
||||
let struct_name_private = format_ident!("__{}", struct_name);
|
||||
|
||||
// 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 =
|
||||
};
|
||||
attribute.asset_parser.to_tokens(&mut linker_tokens);
|
||||
|
||||
let asset = match attribute.asset_parser.asset.as_ref() {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
let err = err.to_string();
|
||||
tokens.append_all(quote! { compile_error!(#err) });
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let css = std::fs::read_to_string(asset).expect("Unable to read css module file");
|
||||
|
||||
let mut values = Vec::new();
|
||||
|
||||
let hash = create_module_hash(asset);
|
||||
let class_mappings = get_class_mappings(css.as_str(), hash.as_str()).expect("Invalid css");
|
||||
|
||||
// Generate class struct field tokens.
|
||||
for (old_class, new_class) in class_mappings.iter() {
|
||||
let as_snake = to_snake_case(old_class);
|
||||
|
||||
let ident = Ident::new(&as_snake, Span::call_site());
|
||||
values.push(quote! {
|
||||
pub const #ident: #struct_name_private::__CssIdent = #struct_name_private::__CssIdent { inner: #new_class };
|
||||
});
|
||||
}
|
||||
|
||||
// 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 #struct_name_private {
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#linker_tokens;
|
||||
|
||||
// Css ident class for deref stylesheet inclusion.
|
||||
pub 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)]
|
||||
#struct_vis struct #struct_name {}
|
||||
|
||||
impl #struct_name {
|
||||
#( #values )*
|
||||
}
|
||||
|
||||
impl dioxus::core::IntoAttributeValue for #struct_name_private::__CssIdent {
|
||||
fn into_value(self) -> dioxus::core::AttributeValue {
|
||||
dioxus::core::AttributeValue::Text(self.to_string())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert camel and kebab case to snake case.
|
||||
|
||||
Reference in new issue
Block a user