use crate::*;
const PSEUDO_KEYWORDS: &[(&str, &str)] = &[
("hover", ":hover"),
("focus", ":focus"),
("focus_within", ":focus-within"),
("focus_visible", ":focus-visible"),
("active", ":active"),
("visited", ":visited"),
("disabled", ":disabled"),
("enabled", ":enabled"),
("checked", ":checked"),
("readonly", ":read-only"),
("readwrite", ":read-write"),
("required", ":required"),
("optional", ":optional"),
("valid", ":valid"),
("invalid", ":invalid"),
("in_range", ":in-range"),
("out_of_range", ":out-of-range"),
("placeholder_shown", ":placeholder-shown"),
("first_child", ":first-child"),
("last_child", ":last-child"),
("only_child", ":only-child"),
("first_of_type", ":first-of-type"),
("last_of_type", ":last-of-type"),
("only_of_type", ":only-of-type"),
("root", ":root"),
("empty", ":empty"),
("target", ":target"),
("link", ":link"),
("any_link", ":any-link"),
("before", "::before"),
("after", "::after"),
("first_line", "::first-line"),
("first_letter", "::first-letter"),
("selection", "::selection"),
("placeholder", "::placeholder"),
("backdrop", "::backdrop"),
("marker", "::marker"),
("spelling_error", "::spelling-error"),
("grammar_error", "::grammar-error"),
];
fn lookup_pseudo_selector(keyword: &str) -> Option<&'static str> {
PSEUDO_KEYWORDS
.iter()
.find(|(kw, _)| *kw == keyword)
.map(|(_, selector)| *selector)
}
impl Parse for ClassInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut classes: Vec<ClassDef> = Vec::new();
while !input.is_empty() {
let visibility: syn::Visibility = input.parse()?;
let name: Ident = input.parse()?;
let params: Option<Vec<ClassParam>> = if input.peek(syn::token::Paren) {
let param_content;
syn::parenthesized!(param_content in input);
let mut param_list: Vec<ClassParam> = Vec::new();
while !param_content.is_empty() {
let param_name: Ident = param_content.parse()?;
param_content.parse::<Token![:]>()?;
let ty: syn::Type = param_content.parse()?;
param_list.push(ClassParam {
name: param_name,
ty,
});
if param_content.peek(Token![,]) {
param_content.parse::<Token![,]>()?;
}
}
Some(param_list)
} else {
None
};
let content;
braced!(content in input);
let mut properties: Vec<(String, ClassPropValue)> = Vec::new();
let mut pseudo_blocks: Vec<PseudoBlock> = Vec::new();
let mut media_blocks: Vec<MediaBlock> = Vec::new();
while !content.is_empty() {
if content.peek(syn::Ident) {
let forked = content.fork();
let keyword: Ident = forked.parse()?;
let keyword_str: String = keyword.to_string();
if lookup_pseudo_selector(&keyword_str).is_some()
&& forked.peek(syn::token::Brace)
{
content.parse::<Ident>()?;
let selector: &'static str = lookup_pseudo_selector(&keyword_str)
.expect("pseudo selector lookup should succeed after check");
let block_content;
braced!(block_content in content);
let mut block_properties: Vec<(String, ClassPropValue)> = Vec::new();
while !block_content.is_empty() {
let css_key: String = parse_kebab_name(&block_content)?;
block_content.parse::<Token![:]>()?;
let expr: Expr = block_content.parse()?;
let expanded: proc_macro2::TokenStream = expand_var_macros(&expr);
let prop_value: ClassPropValue = ClassPropValue::Expr(expanded);
block_properties.push((css_key, prop_value));
if block_content.peek(Semi) {
block_content.parse::<Semi>()?;
}
}
pseudo_blocks.push(PseudoBlock {
selector: selector.to_string(),
properties: block_properties,
});
continue;
} else if (keyword_str == "nth_child" || keyword_str == "nth_last_child")
&& forked.peek(syn::token::Paren)
{
let forked2 = forked;
let _paren_content;
syn::parenthesized!(_paren_content in forked2);
if forked2.peek(syn::token::Brace) {
content.parse::<Ident>()?;
let paren_content;
syn::parenthesized!(paren_content in content);
let arg_tokens: proc_macro2::TokenStream = paren_content.parse()?;
let arg_str: String = arg_tokens.to_string().replace(' ', "");
let selector: String =
format!(":{}({})", keyword_str.replace('_', "-"), arg_str);
let block_content;
braced!(block_content in content);
let mut block_properties: Vec<(String, ClassPropValue)> = Vec::new();
while !block_content.is_empty() {
let css_key: String = parse_kebab_name(&block_content)?;
block_content.parse::<Token![:]>()?;
let expr: Expr = block_content.parse()?;
let expanded: proc_macro2::TokenStream = expand_var_macros(&expr);
let prop_value: ClassPropValue = ClassPropValue::Expr(expanded);
block_properties.push((css_key, prop_value));
if block_content.peek(Semi) {
block_content.parse::<Semi>()?;
}
}
pseudo_blocks.push(PseudoBlock {
selector,
properties: block_properties,
});
continue;
}
} else if keyword_str == "media" && forked.peek2(syn::token::Paren) {
let forked2 = forked;
forked2.parse::<Ident>()?;
let paren_content2;
syn::parenthesized!(paren_content2 in forked2);
if forked2.peek(syn::token::Brace) {
content.parse::<Ident>()?;
let query_content;
syn::parenthesized!(query_content in content);
let query_expr: Expr = query_content.parse()?;
let query_str: String = match &query_expr {
Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) => lit_str.value(),
_ => query_expr.to_token_stream().to_string().replace(' ', ""),
};
let block_content;
braced!(block_content in content);
let mut block_properties: Vec<(String, ClassPropValue)> = Vec::new();
while !block_content.is_empty() {
let css_key: String = parse_kebab_name(&block_content)?;
block_content.parse::<Token![:]>()?;
let expr: Expr = block_content.parse()?;
let expanded: proc_macro2::TokenStream = expand_var_macros(&expr);
let prop_value: ClassPropValue = ClassPropValue::Expr(expanded);
block_properties.push((css_key, prop_value));
if block_content.peek(Semi) {
block_content.parse::<Semi>()?;
}
}
media_blocks.push(MediaBlock {
query: query_str,
properties: block_properties,
});
continue;
}
}
}
let css_key: String = parse_kebab_name(&content)?;
content.parse::<Token![:]>()?;
let expr: Expr = content.parse()?;
let expanded: proc_macro2::TokenStream = expand_var_macros(&expr);
let prop_value: ClassPropValue = ClassPropValue::Expr(expanded);
properties.push((css_key, prop_value));
if content.peek(Semi) {
content.parse::<Semi>()?;
}
}
classes.push(ClassDef {
visibility,
name,
params,
properties,
pseudo_blocks,
media_blocks,
});
}
Ok(ClassInput { classes })
}
}
fn pseudo_blocks_to_tokens(pseudo_blocks: &[PseudoBlock]) -> Option<proc_macro2::TokenStream> {
if pseudo_blocks.is_empty() {
return None;
}
let parts: Vec<proc_macro2::TokenStream> = pseudo_blocks
.iter()
.map(|block| {
let selector: &str = &block.selector;
let style_parts: Vec<proc_macro2::TokenStream> = block
.properties
.iter()
.map(|(key, value)| match value {
ClassPropValue::Expr(expr) => {
quote! { #key.to_string() + ": " + &(#expr).to_string() + "; " }
}
})
.collect();
quote! {
euv_core::PseudoRule::new(
#selector.to_string(),
[#(#style_parts),*].concat()
)
}
})
.collect();
Some(quote! { vec![#(#parts),*] })
}
fn media_blocks_to_tokens(media_blocks: &[MediaBlock]) -> Option<proc_macro2::TokenStream> {
if media_blocks.is_empty() {
return None;
}
let parts: Vec<proc_macro2::TokenStream> = media_blocks
.iter()
.map(|block| {
let query: &str = &block.query;
let style_parts: Vec<proc_macro2::TokenStream> = block
.properties
.iter()
.map(|(key, value)| match value {
ClassPropValue::Expr(expr) => {
quote! { #key.to_string() + ": " + &(#expr).to_string() + "; " }
}
})
.collect();
quote! {
euv_core::MediaRule::new(
#query.to_string(),
[#(#style_parts),*].concat()
)
}
})
.collect();
Some(quote! { vec![#(#parts),*] })
}
fn pseudo_blocks_to_static_string(pseudo_blocks: &[PseudoBlock]) -> String {
let mut result: String = String::new();
for block in pseudo_blocks {
result.push_str(&block.selector);
result.push_str(" { ");
for (key, value) in &block.properties {
let ClassPropValue::Expr(expr) = value;
result.push_str(key);
result.push_str(": ");
result.push_str(&expr_to_string(expr));
result.push_str("; ");
}
result.push('}');
}
result
}
fn media_blocks_to_static_string(media_blocks: &[MediaBlock]) -> String {
let mut result: String = String::new();
for block in media_blocks {
result.push_str("@media ");
result.push_str(&block.query);
result.push_str(" { ");
for (key, value) in &block.properties {
let ClassPropValue::Expr(expr) = value;
result.push_str(key);
result.push_str(": ");
result.push_str(&expr_to_string(expr));
result.push_str("; ");
}
result.push('}');
}
result
}
impl ToTokens for ClassDef {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let vis: &syn::Visibility = &self.visibility;
let name: &Ident = &self.name;
let class_name_str: String = name.to_string();
let has_extra: bool = !self.pseudo_blocks.is_empty() || !self.media_blocks.is_empty();
match &self.params {
Some(params) => {
let param_defs: Vec<proc_macro2::TokenStream> = params
.iter()
.map(|param| {
let param_name: &Ident = ¶m.name;
let ty: &syn::Type = ¶m.ty;
quote! { #param_name: #ty }
})
.collect();
let param_names: Vec<&Ident> = params.iter().map(|p| &p.name).collect();
let css_string_parts: Vec<proc_macro2::TokenStream> = self
.properties
.iter()
.map(|(key, value)| match value {
ClassPropValue::Expr(expr) => {
quote! { #key.to_string() + ": " + &(#expr) + "; " }
}
})
.collect();
if has_extra {
let pseudo_expr: proc_macro2::TokenStream =
pseudo_blocks_to_tokens(&self.pseudo_blocks)
.unwrap_or_else(|| quote! { vec![] });
let media_expr: proc_macro2::TokenStream =
media_blocks_to_tokens(&self.media_blocks)
.unwrap_or_else(|| quote! { vec![] });
tokens.extend(quote! {
#vis fn #name(#(#param_defs),*) -> euv_core::CssClass {
let __css_string: String = [#(#css_string_parts),*].concat();
let __unique_name: String = format!("{}-{}", #class_name_str, [#(format!("{:?}", #param_names)),*].join("-"));
euv_core::CssClass::new_with_rules(__unique_name, __css_string, #pseudo_expr, #media_expr)
}
});
} else {
tokens.extend(quote! {
#vis fn #name(#(#param_defs),*) -> euv_core::CssClass {
let __css_string: String = [#(#css_string_parts),*].concat();
let __unique_name: String = format!("{}-{}", #class_name_str, [#(format!("{:?}", #param_names)),*].join("-"));
euv_core::CssClass::new(__unique_name, __css_string)
}
});
}
}
None => {
let name_span: Span = name.span();
let const_name: Ident = Ident::new(&class_name_str.to_uppercase(), name.span());
let const_name_token: proc_macro2::TokenStream =
quote_spanned!(name_span=> #const_name);
let fn_name_token: proc_macro2::TokenStream = quote_spanned!(name_span=> #name);
let all_static: bool = self.properties.iter().all(|(_, value)| {
let ClassPropValue::Expr(expr) = value;
is_static_string_expr(expr)
}) && self.pseudo_blocks.iter().all(|block| {
block.properties.iter().all(|(_, value)| {
let ClassPropValue::Expr(expr) = value;
is_static_string_expr(expr)
})
}) && self.media_blocks.iter().all(|block| {
block.properties.iter().all(|(_, value)| {
let ClassPropValue::Expr(expr) = value;
is_static_string_expr(expr)
})
});
if all_static {
let mut css_string: String = String::new();
for (key, value) in &self.properties {
let ClassPropValue::Expr(expr) = value;
css_string.push_str(key);
css_string.push_str(": ");
css_string.push_str(&expr_to_string(expr));
css_string.push_str("; ");
}
if has_extra {
let pseudo_static: String =
pseudo_blocks_to_static_string(&self.pseudo_blocks);
let media_static: String =
media_blocks_to_static_string(&self.media_blocks);
tokens.extend(quote! {
#vis fn #fn_name_token() -> &'static euv_core::CssClass {
static #const_name_token: std::sync::OnceLock<euv_core::CssClass> = std::sync::OnceLock::new();
#const_name_token.get_or_init(|| {
euv_core::CssClass::new_with_rules(
#class_name_str.to_string(),
#css_string.to_string(),
euv_core::CssClass::parse_pseudo_rules(#pseudo_static),
euv_core::CssClass::parse_media_rules(#media_static),
)
})
}
});
} else {
tokens.extend(quote! {
#vis fn #fn_name_token() -> &'static euv_core::CssClass {
static #const_name_token: std::sync::OnceLock<euv_core::CssClass> = std::sync::OnceLock::new();
#const_name_token.get_or_init(|| {
euv_core::CssClass::new(#class_name_str.to_string(), #css_string.to_string())
})
}
});
}
} else {
let css_string_parts: Vec<proc_macro2::TokenStream> = self
.properties
.iter()
.map(|(key, value)| match value {
ClassPropValue::Expr(expr) => {
quote! { #key.to_string() + ": " + &(#expr).to_string() + "; " }
}
})
.collect();
if has_extra {
let pseudo_expr: proc_macro2::TokenStream =
pseudo_blocks_to_tokens(&self.pseudo_blocks)
.unwrap_or_else(|| quote! { vec![] });
let media_expr: proc_macro2::TokenStream =
media_blocks_to_tokens(&self.media_blocks)
.unwrap_or_else(|| quote! { vec![] });
tokens.extend(quote! {
#vis fn #fn_name_token() -> &'static euv_core::CssClass {
static #const_name_token: std::sync::OnceLock<euv_core::CssClass> = std::sync::OnceLock::new();
#const_name_token.get_or_init(|| {
let __css_string: String = [#(#css_string_parts),*].concat();
euv_core::CssClass::new_with_rules(#class_name_str.to_string(), __css_string, #pseudo_expr, #media_expr)
})
}
});
} else {
tokens.extend(quote! {
#vis fn #fn_name_token() -> &'static euv_core::CssClass {
static #const_name_token: std::sync::OnceLock<euv_core::CssClass> = std::sync::OnceLock::new();
#const_name_token.get_or_init(|| {
let __css_string: String = [#(#css_string_parts),*].concat();
euv_core::CssClass::new(#class_name_str.to_string(), __css_string)
})
}
});
}
}
}
}
}
}
impl ToTokens for ClassInput {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
for class_def in &self.classes {
class_def.to_tokens(tokens);
}
}
}