use super::class::{ClassMode, parse_single_class_with_mode};
use super::tables::{
COLOR_FAMILIES, COLOR_SHADES, LENGTH_CLASS_SPECS, dynamic_common_classes, lookup_color,
};
use proc_macro2::{Span, TokenStream};
use quote::quote;
use std::cell::RefCell;
thread_local! {
static COMMON_CLASS_MATCHES_STR: RefCell<Option<String>> = const { RefCell::new(None) };
static COLOR_FALLBACK_STR: RefCell<Option<String>> = const { RefCell::new(None) };
static NUMERIC_FALLBACK_STR: RefCell<Option<String>> = const { RefCell::new(None) };
}
fn get_cached_common_class_matches() -> TokenStream {
COMMON_CLASS_MATCHES_STR.with(|cell| {
let mut borrow = cell.borrow_mut();
let s = borrow.get_or_insert_with(|| {
generate_common_class_matches()
.into_iter()
.map(|ts| ts.to_string())
.collect::<String>()
});
s.parse::<TokenStream>()
.expect("cached match arms are valid")
})
}
fn get_cached_color_fallback() -> TokenStream {
COLOR_FALLBACK_STR.with(|cell| {
let mut borrow = cell.borrow_mut();
let s = borrow.get_or_insert_with(|| generate_color_fallback_code().to_string());
s.parse::<TokenStream>()
.expect("cached color fallback is valid")
})
}
fn get_cached_numeric_fallback() -> TokenStream {
NUMERIC_FALLBACK_STR.with(|cell| {
let mut borrow = cell.borrow_mut();
let s = borrow.get_or_insert_with(|| generate_numeric_fallback_code().to_string());
s.parse::<TokenStream>()
.expect("cached numeric fallback is valid")
})
}
pub(crate) fn generate_dynamic_class_code_with_mode(
class_expr: &syn::Expr,
mode: ClassMode,
) -> TokenStream {
let common_classes = get_cached_common_class_matches();
let color_fallbacks = get_cached_color_fallback();
let numeric_fallbacks = get_cached_numeric_fallback();
let unknown_fallback = match mode {
ClassMode::Permissive => quote! {
#[cfg(debug_assertions)]
if !class.is_empty() {
eprintln!(
"[gpui-rsx] warning: 动态 class {:?} 被忽略(不支持的 class 类型)\n \
提示:改用字符串字面量 class=\"{}\" 可支持所有 class",
class, class
);
}
el
},
ClassMode::Strict => quote! {
panic!(
"[gpui-rsx] unsupported dynamic class {:?} in strict mode. \
Use rsx! or rsx_permissive! to ignore unsupported dynamic classes.",
class
);
},
};
quote! {
{
#[inline(never)]
fn __rsx_apply_class<E: Styled>(el: E, class: &str) -> E {
match class {
#common_classes
_ => {
#color_fallbacks
#numeric_fallbacks
#unknown_fallback
}
}
}
let __class_expr = #class_expr;
let __class_str: &str = __class_expr.as_ref();
if __class_str.is_empty() {
__el
} else {
__class_str.split_ascii_whitespace().fold(__el, __rsx_apply_class)
}
}
}
}
fn generate_color_fallback_code() -> TokenStream {
let shade_arms = COLOR_SHADES.iter().enumerate().map(|(idx, shade)| {
quote! { #shade => #idx, }
});
let family_arms = COLOR_FAMILIES.iter().map(|family| {
let values = COLOR_SHADES.iter().map(|shade| {
let key = format!("{family}_{shade}");
lookup_color(&key).expect("COLOR_FAMILIES/COLOR_SHADES must match lookup_color")
});
quote! {
#family => {
const VALUES: [u32; 11] = [#(#values),*];
Some(VALUES[shade_index])
}
}
});
quote! {
fn __rsx_hex_digit(byte: u8) -> Option<u32> {
match byte {
b'0'..=b'9' => Some((byte - b'0') as u32),
b'a'..=b'f' => Some((byte - b'a' + 10) as u32),
b'A'..=b'F' => Some((byte - b'A' + 10) as u32),
_ => None,
}
}
fn __rsx_parse_hex_color(color: &str) -> Option<u32> {
let inner = color.strip_prefix("[#")?.strip_suffix(']')?;
let bytes = inner.as_bytes();
match bytes.len() {
8 => u32::from_str_radix(inner, 16).ok(),
6 => u32::from_str_radix(inner, 16).ok(),
4 => {
let r = __rsx_hex_digit(bytes[0])?;
let g = __rsx_hex_digit(bytes[1])?;
let b = __rsx_hex_digit(bytes[2])?;
let a = __rsx_hex_digit(bytes[3])?;
let expand = |n: u32| (n << 4) | n;
Some(expand(r) << 24 | expand(g) << 16 | expand(b) << 8 | expand(a))
}
3 => {
let r = __rsx_hex_digit(bytes[0])?;
let g = __rsx_hex_digit(bytes[1])?;
let b = __rsx_hex_digit(bytes[2])?;
Some(r << 20 | r << 16 | g << 12 | g << 8 | b << 4 | b)
}
_ => None,
}
}
fn __rsx_parse_u8_component(raw: &str) -> Option<u8> {
raw.trim().parse::<u8>().ok()
}
fn __rsx_parse_alpha_component(raw: &str) -> Option<u8> {
let value = raw.trim().parse::<f32>().ok()?;
if !value.is_finite() || !(0.0..=1.0).contains(&value) {
return None;
}
Some((value * 255.0).round() as u8)
}
fn __rsx_parse_color_function(color: &str) -> Option<(u32, bool)> {
let inner = color.strip_prefix('[')?.strip_suffix(']')?;
if let Some(args) = inner.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) {
let mut parts = args.split(',');
let r = __rsx_parse_u8_component(parts.next()?)?;
let g = __rsx_parse_u8_component(parts.next()?)?;
let b = __rsx_parse_u8_component(parts.next()?)?;
if parts.next().is_some() {
return None;
}
return Some((((r as u32) << 16) | ((g as u32) << 8) | b as u32, false));
}
if let Some(args) = inner.strip_prefix("rgba(").and_then(|s| s.strip_suffix(')')) {
let mut parts = args.split(',');
let r = __rsx_parse_u8_component(parts.next()?)?;
let g = __rsx_parse_u8_component(parts.next()?)?;
let b = __rsx_parse_u8_component(parts.next()?)?;
let a = __rsx_parse_alpha_component(parts.next()?)?;
if parts.next().is_some() {
return None;
}
return Some((
((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | a as u32,
true,
));
}
None
}
fn __rsx_parse_named_color(color: &str) -> Option<u32> {
if color == "black" {
return Some(0x000000);
}
if color == "white" {
return Some(0xffffff);
}
let (family, shade) = color.rsplit_once('-')?;
let shade_index = match shade {
#(#shade_arms)*
_ => return None,
};
match family {
#(#family_arms,)*
_ => None,
}
}
fn __rsx_parse_color(color: &str) -> Option<(u32, bool)> {
if let Some(hex) = __rsx_parse_hex_color(color) {
let hex_inner = color.strip_prefix("[#")?.strip_suffix(']')?;
let is_rgba = hex_inner.len() == 8 || hex_inner.len() == 4;
return Some((hex, is_rgba));
}
__rsx_parse_color_function(color)
.or_else(|| __rsx_parse_named_color(color).map(|color| (color, false)))
}
if let Some(rest) = class.strip_prefix("text-")
&& let Some((color, is_rgba)) = __rsx_parse_color(rest)
{
if is_rgba {
return el.text_color(rgba(color));
}
return el.text_color(rgb(color));
}
if let Some(rest) = class.strip_prefix("bg-")
&& let Some((color, is_rgba)) = __rsx_parse_color(rest)
{
if is_rgba {
return el.bg(rgba(color));
}
return el.bg(rgb(color));
}
if let Some(rest) = class.strip_prefix("border-")
&& let Some((color, is_rgba)) = __rsx_parse_color(rest)
{
if is_rgba {
return el.border_color(rgba(color));
}
return el.border_color(rgb(color));
}
}
}
fn generate_numeric_fallback_code() -> TokenStream {
let length_fallbacks = LENGTH_CLASS_SPECS.iter().map(generate_length_fallback);
let usize_fallbacks = [("line-clamp-", "line_clamp")]
.into_iter()
.map(|(prefix, method)| generate_integer_fallback(prefix, method, "usize"));
let u16_fallbacks = [
("col-span-", "col_span"),
("row-span-", "row_span"),
("grid-cols-", "grid_cols"),
("grid-rows-", "grid_rows"),
]
.into_iter()
.map(|(prefix, method)| generate_integer_fallback(prefix, method, "u16"));
let i16_fallbacks = [
("col-start-", "col_start"),
("col-end-", "col_end"),
("row-start-", "row_start"),
("row-end-", "row_end"),
]
.into_iter()
.map(|(prefix, method)| generate_integer_fallback(prefix, method, "i16"));
quote! {
trait __RsxFiniteFloat {
fn __rsx_finite(self) -> Result<f32, ()>;
}
impl __RsxFiniteFloat for Result<f32, std::num::ParseFloatError> {
fn __rsx_finite(self) -> Result<f32, ()> {
match self {
Ok(n) if n.is_finite() => Ok(n),
_ => Err(()),
}
}
}
#(#length_fallbacks)*
if let Some(rest) = class.strip_prefix("opacity-") {
if let Ok(n) = rest.parse::<f32>().__rsx_finite() { return el.opacity(n / 100.0); }
}
#(#usize_fallbacks)*
#(#u16_fallbacks)*
#(#i16_fallbacks)*
}
}
fn generate_length_fallback(spec: &super::tables::LengthClassSpec) -> TokenStream {
let prefix = spec.prefix;
let method = syn::Ident::new(spec.method, Span::call_site());
let percent = if spec.family.allows_percent() {
quote! {
if let Some(raw) = inner.strip_suffix('%') {
if let Ok(n) = raw.parse::<f32>().__rsx_finite() {
return el.#method(relative(n / 100.0));
}
}
}
} else {
quote! {}
};
let fraction = if spec.family.allows_fraction() {
quote! {
if let Some((num, den)) = rest.split_once('/') {
if let (Ok(num), Ok(den)) = (
num.parse::<f32>().__rsx_finite(),
den.parse::<f32>().__rsx_finite(),
) {
if den > 0.0 {
return el.#method(relative(num / den));
}
}
}
}
} else {
quote! {}
};
quote! {
if let Some(rest) = class.strip_prefix(#prefix) {
if let Some(inner) = rest.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
if let Some(raw) = inner.strip_suffix("px") {
if let Ok(n) = raw.parse::<f32>().__rsx_finite() {
return el.#method(px(n));
}
}
if let Some(raw) = inner.strip_suffix("rem") {
if let Ok(n) = raw.parse::<f32>().__rsx_finite() {
return el.#method(rems(n));
}
}
#percent
}
#fraction
if let Ok(n) = rest.parse::<f32>().__rsx_finite() {
return el.#method(px(n));
}
}
}
}
fn generate_integer_fallback(prefix: &'static str, method: &'static str, ty: &str) -> TokenStream {
let method = syn::Ident::new(method, Span::call_site());
let ty: TokenStream = ty.parse().expect("integer fallback type is valid");
quote! {
if let Some(rest) = class.strip_prefix(#prefix) {
if let Ok(n) = rest.parse::<#ty>() {
return el.#method(n);
}
}
}
}
fn generate_common_class_matches() -> Vec<TokenStream> {
let mut matches = Vec::new();
for class_str in dynamic_common_classes() {
let method_call = parse_dynamic_common_class(class_str);
matches.push(quote! {
#class_str => #method_call,
});
}
matches
}
fn parse_dynamic_common_class(class: &str) -> TokenStream {
match class {
"debug-outline" => quote! {
{
#[cfg(debug_assertions)]
{
el.debug()
}
#[cfg(not(debug_assertions))]
{
el
}
}
},
"flex-grow-0" => quote! {
{
let mut el = el;
el.style().flex_grow = Some(0.0);
el
}
},
"items-stretch" => quote! {
{
let mut el = el;
el.style().align_items = Some(AlignItems::Stretch);
el
}
},
"content-stretch" => quote! {
{
let mut el = el;
el.style().align_content = Some(AlignContent::Stretch);
el
}
},
"justify-evenly" => quote! {
{
let mut el = el;
el.style().justify_content = Some(JustifyContent::SpaceEvenly);
el
}
},
"self-start" | "self-flex-start" => quote! {
{
let mut el = el;
el.style().align_self = Some(AlignItems::FlexStart);
el
}
},
"self-end" | "self-flex-end" => quote! {
{
let mut el = el;
el.style().align_self = Some(AlignItems::FlexEnd);
el
}
},
"self-center" => quote! {
{
let mut el = el;
el.style().align_self = Some(AlignItems::Center);
el
}
},
"self-baseline" => quote! {
{
let mut el = el;
el.style().align_self = Some(AlignItems::Baseline);
el
}
},
"self-stretch" => quote! {
{
let mut el = el;
el.style().align_self = Some(AlignItems::Stretch);
el
}
},
_ => {
let method_call = parse_single_class_with_mode(class, ClassMode::Permissive);
quote! { el #method_call }
}
}
}